Skip to content

Commit a73d9ce

Browse files
committed
Update to support optional email register and signin
1 parent 5277282 commit a73d9ce

14 files changed

Lines changed: 180 additions & 16 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Environment variables (will overwrite other server configs)
131131
| HMD_GOOGLE_CLIENTID | no example | Google API client id |
132132
| HMD_GOOGLE_CLIENTSECRET | no example | Google API client secret |
133133
| HMD_IMGUR_CLIENTID | no example | Imgur API client id |
134+
| HMD_EMAIL | `true` or `false` | set to allow email register and signin |
134135
| HMD_IMAGE_UPLOAD_TYPE | `imgur`, `s3` or `filesystem` | Where to upload image. For S3, see our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
135136
| HMD_S3_ACCESS_KEY_ID | no example | AWS access key id |
136137
| HMD_S3_SECRET_ACCESS_KEY | no example | AWS secret key |
@@ -171,6 +172,7 @@ Server settings `config.json`
171172
| heartbeatinterval | `5000` | socket.io heartbeat interval |
172173
| heartbeattimeout | `10000` | socket.io heartbeat timeout |
173174
| documentmaxlength | `100000` | note max length |
175+
| email | `true` or `false` | set to allow email register and signin |
174176
| imageUploadType | `imgur`(default), `s3` or `filesystem` | Where to upload image
175177
| s3 | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION", "bucket": "YOUR_S3_BUCKET_NAME" }` | When `imageUploadType` be setted to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
176178

app.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var morgan = require('morgan');
1717
var passportSocketIo = require("passport.socketio");
1818
var helmet = require('helmet');
1919
var i18n = require('i18n');
20+
var flash = require('connect-flash');
21+
var validator = require('validator');
2022

2123
//core
2224
var config = require("./lib/config.js");
@@ -145,6 +147,8 @@ app.use(function (req, res, next) {
145147
}
146148
});
147149

150+
app.use(flash());
151+
148152
//passport
149153
app.use(passport.initialize());
150154
app.use(passport.session());
@@ -362,6 +366,47 @@ if (config.google) {
362366
failureRedirect: config.serverurl + '/'
363367
}));
364368
}
369+
// email auth
370+
if (config.email) {
371+
app.post('/register', urlencodedParser, function (req, res, next) {
372+
if (!req.body.email || !req.body.password) return response.errorBadRequest(res);
373+
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res);
374+
models.User.findOrCreate({
375+
where: {
376+
email: req.body.email
377+
},
378+
defaults: {
379+
password: req.body.password
380+
}
381+
}).spread(function (user, created) {
382+
if (user) {
383+
if (created) {
384+
if (config.debug) logger.info('user registered: ' + user.id);
385+
req.flash('info', "You've successfully registered, please signin.");
386+
} else {
387+
if (config.debug) logger.info('user found: ' + user.id);
388+
req.flash('error', "This email has been used, please try another one.");
389+
}
390+
return res.redirect(config.serverurl + '/');
391+
}
392+
req.flash('error', "Failed to register your account, please try again.");
393+
return res.redirect(config.serverurl + '/');
394+
}).catch(function (err) {
395+
logger.error('auth callback failed: ' + err);
396+
return response.errorInternalError(res);
397+
});
398+
});
399+
app.post('/login', urlencodedParser, function (req, res, next) {
400+
if (!req.body.email || !req.body.password) return response.errorBadRequest(res);
401+
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res);
402+
setReturnToFromReferer(req);
403+
passport.authenticate('local', {
404+
successReturnToOrRedirect: config.serverurl + '/',
405+
failureRedirect: config.serverurl + '/',
406+
failureFlash: 'Invalid email or password.'
407+
})(req, res, next);
408+
});
409+
}
365410
//logout
366411
app.get('/logout', function (req, res) {
367412
if (config.debug && req.isAuthenticated())
@@ -389,7 +434,7 @@ app.get('/me', function (req, res) {
389434
}).then(function (user) {
390435
if (!user)
391436
return response.errorNotFound(res);
392-
var profile = models.User.parseProfile(user.profile);
437+
var profile = models.User.getProfile(user);
393438
res.send({
394439
status: 'ok',
395440
id: req.user.id,

lib/auth.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ var GithubStrategy = require('passport-github').Strategy;
77
var GitlabStrategy = require('passport-gitlab2').Strategy;
88
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
99
var GoogleStrategy = require('passport-google-oauth20').Strategy;
10+
var LocalStrategy = require('passport-local').Strategy;
11+
var validator = require('validator');
1012

1113
//core
1214
var config = require('./config.js');
@@ -35,12 +37,10 @@ function callback(accessToken, refreshToken, profile, done) {
3537
if (user.accessToken != accessToken) {
3638
user.accessToken = accessToken;
3739
needSave = true;
38-
3940
}
4041
if (user.refreshToken != refreshToken) {
4142
user.refreshToken = refreshToken;
4243
needSave = true;
43-
4444
}
4545
if (needSave) {
4646
user.save().then(function () {
@@ -57,7 +57,7 @@ function callback(accessToken, refreshToken, profile, done) {
5757
}).catch(function (err) {
5858
logger.error('auth callback failed: ' + err);
5959
return done(err, null);
60-
})
60+
});
6161
}
6262

6363
//facebook
@@ -109,4 +109,25 @@ if (config.google) {
109109
clientSecret: config.google.clientSecret,
110110
callbackURL: config.serverurl + '/auth/google/callback'
111111
}, callback));
112+
}
113+
// email
114+
if (config.email) {
115+
passport.use(new LocalStrategy({
116+
usernameField: 'email'
117+
},
118+
function(email, password, done) {
119+
if (!validator.isEmail(email)) return done(null, false);
120+
models.User.findOne({
121+
where: {
122+
email: email
123+
}
124+
}).then(function (user) {
125+
if (!user) return done(null, false);
126+
if (!user.verifyPassword(password)) return done(null, false);
127+
return done(null, user);
128+
}).catch(function (err) {
129+
logger.error(err);
130+
return done(err);
131+
});
132+
}));
112133
}

lib/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ var google = (process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSE
9494
clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET
9595
} : config.google || false;
9696
var imgur = process.env.HMD_IMGUR_CLIENTID || config.imgur || false;
97+
var email = process.env.HMD_EMAIL || config.email || false;
9798

9899
function getserverurl() {
99100
var url = '';
@@ -151,6 +152,7 @@ module.exports = {
151152
dropbox: dropbox,
152153
google: google,
153154
imgur: imgur,
155+
email: email,
154156
imageUploadType: imageUploadType,
155157
s3: s3,
156158
s3bucket: s3bucket
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
3+
module.exports = {
4+
up: function (queryInterface, Sequelize) {
5+
queryInterface.addColumn('Users', 'email', Sequelize.TEXT);
6+
queryInterface.addColumn('Users', 'password', Sequelize.TEXT);
7+
},
8+
9+
down: function (queryInterface, Sequelize) {
10+
queryInterface.removeColumn('Users', 'email', Sequelize.TEXT);
11+
queryInterface.removeColumn('Users', 'password', Sequelize.TEXT);
12+
}
13+
};

lib/models/user.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// external modules
44
var md5 = require("blueimp-md5");
55
var Sequelize = require("sequelize");
6+
var scrypt = require('scrypt');
67

78
// core
89
var logger = require("../logger.js");
@@ -29,8 +30,30 @@ module.exports = function (sequelize, DataTypes) {
2930
},
3031
refreshToken: {
3132
type: DataTypes.STRING
33+
},
34+
email: {
35+
type: Sequelize.TEXT,
36+
validate: {
37+
isEmail: true
38+
}
39+
},
40+
password: {
41+
type: Sequelize.TEXT,
42+
set: function(value) {
43+
var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString("hex");
44+
this.setDataValue('password', hash);
45+
}
3246
}
3347
}, {
48+
instanceMethods: {
49+
verifyPassword: function(attempt) {
50+
if (scrypt.verifyKdfSync(new Buffer(this.password, "hex"), attempt)) {
51+
return this;
52+
} else {
53+
return false;
54+
}
55+
}
56+
},
3457
classMethods: {
3558
associate: function (models) {
3659
User.hasMany(models.Note, {
@@ -42,6 +65,9 @@ module.exports = function (sequelize, DataTypes) {
4265
constraints: false
4366
});
4467
},
68+
getProfile: function (user) {
69+
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null);
70+
},
4571
parseProfile: function (profile) {
4672
try {
4773
var profile = JSON.parse(profile);
@@ -81,6 +107,13 @@ module.exports = function (sequelize, DataTypes) {
81107
break;
82108
}
83109
return photo;
110+
},
111+
parseProfileByEmail: function (email) {
112+
var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email);
113+
return {
114+
name: email.substring(0, email.lastIndexOf("@")),
115+
photo: photoUrl += '?s=96'
116+
};
84117
}
85118
}
86119
});

lib/realtime.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function updateNote(note, callback) {
131131
}
132132
}).then(function (user) {
133133
if (!user) return callback(null, null);
134-
note.lastchangeuserprofile = models.User.parseProfile(user.profile);
134+
note.lastchangeuserprofile = models.User.getProfile(user);
135135
return finishUpdateNote(note, _note, callback);
136136
}).catch(function (err) {
137137
logger.error(err);
@@ -455,10 +455,10 @@ function startConnection(socket) {
455455
return failConnection(404, 'note not found', socket);
456456
}
457457
var owner = note.ownerId;
458-
var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null;
458+
var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null;
459459

460460
var lastchangeuser = note.lastchangeuserId;
461-
var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null;
461+
var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null;
462462

463463
var body = LZString.decompressFromBase64(note.content);
464464
var createtime = note.createdAt;
@@ -468,7 +468,7 @@ function startConnection(socket) {
468468
var authors = {};
469469
for (var i = 0; i < note.authors.length; i++) {
470470
var author = note.authors[i];
471-
var profile = models.User.parseProfile(author.user.profile);
471+
var profile = models.User.getProfile(author.user);
472472
authors[author.userId] = {
473473
userid: author.userId,
474474
color: author.color,
@@ -598,7 +598,7 @@ function buildUserOutData(user) {
598598
function updateUserData(socket, user) {
599599
//retrieve user data from passport
600600
if (socket.request.user && socket.request.user.logged_in) {
601-
var profile = models.User.parseProfile(socket.request.user.profile);
601+
var profile = models.User.getProfile(socket.request.user);
602602
user.photo = profile.photo;
603603
user.name = profile.name;
604604
user.userid = socket.request.user.id;

lib/response.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ function showIndex(req, res, next) {
6666
gitlab: config.gitlab,
6767
dropbox: config.dropbox,
6868
google: config.google,
69-
signin: req.isAuthenticated()
69+
email: config.email,
70+
signin: req.isAuthenticated(),
71+
infoMessage: req.flash('info'),
72+
errorMessage: req.flash('error')
7073
});
7174
}
7275

@@ -94,7 +97,8 @@ function responseHackMD(res, note) {
9497
github: config.github,
9598
gitlab: config.gitlab,
9699
dropbox: config.dropbox,
97-
google: config.google
100+
google: config.google,
101+
email: config.email
98102
});
99103
}
100104

@@ -202,9 +206,9 @@ function showPublishNote(req, res, next) {
202206
body: body,
203207
useCDN: config.usecdn,
204208
owner: note.owner ? note.owner.id : null,
205-
ownerprofile: note.owner ? models.User.parseProfile(note.owner.profile) : null,
209+
ownerprofile: note.owner ? models.User.getProfile(note.owner) : null,
206210
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
207-
lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
211+
lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null,
208212
robots: meta.robots || false, //default allow robots
209213
GA: meta.GA,
210214
disqus: meta.disqus
@@ -591,9 +595,9 @@ function showPublishSlide(req, res, next) {
591595
meta: JSON.stringify(obj.meta || {}),
592596
useCDN: config.usecdn,
593597
owner: note.owner ? note.owner.id : null,
594-
ownerprofile: note.owner ? models.User.parseProfile(note.owner.profile) : null,
598+
ownerprofile: note.owner ? models.User.getProfile(note.owner) : null,
595599
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
596-
lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
600+
lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null,
597601
robots: meta.robots || false, //default allow robots
598602
GA: meta.GA,
599603
disqus: meta.disqus

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
"blueimp-md5": "^2.4.0",
1919
"body-parser": "^1.15.2",
2020
"bootstrap": "^3.3.7",
21+
"bootstrap-validator": "^0.11.5",
2122
"chance": "^1.0.4",
2223
"cheerio": "^0.22.0",
2324
"codemirror": "git+https://github.com/hackmdio/CodeMirror.git",
2425
"compression": "^1.6.2",
26+
"connect-flash": "^0.1.1",
2527
"connect-session-sequelize": "^3.2.0",
2628
"cookie": "0.3.1",
2729
"cookie-parser": "1.4.3",
@@ -84,6 +86,7 @@
8486
"passport-github": "^1.1.0",
8587
"passport-gitlab2": "^2.2.0",
8688
"passport-google-oauth20": "^1.0.0",
89+
"passport-local": "^1.0.0",
8790
"passport-twitter": "^1.0.4",
8891
"passport.socketio": "^3.6.2",
8992
"pdfobject": "^2.0.201604172",
@@ -95,6 +98,7 @@
9598
"request": "^2.75.0",
9699
"reveal.js": "^3.3.0",
97100
"sequelize": "^3.24.3",
101+
"scrypt": "^6.0.3",
98102
"select2": "^3.5.2-browserify",
99103
"sequelize-cli": "^2.4.0",
100104
"sharp": "^0.16.2",
@@ -109,6 +113,7 @@
109113
"to-markdown": "^3.0.1",
110114
"toobusy-js": "^0.5.1",
111115
"uws": "^0.11.0",
116+
"validator": "^6.2.0",
112117
"velocity-animate": "^1.3.1",
113118
"visibilityjs": "^1.2.4",
114119
"viz.js": "^1.3.0",

public/css/cover.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ input {
305305
text-align: left;
306306
color: black;
307307
}
308+
.modal-body {
309+
color: black;
310+
}
308311

309312
.btn-file {
310313
position: relative;

0 commit comments

Comments
 (0)