From 424c98e02d275246938ac785589a2c7413228388 Mon Sep 17 00:00:00 2001 From: Menci Date: Sat, 24 Jun 2017 14:25:26 +0800 Subject: [PATCH] Add optional email address verification --- config-example.json | 5 +++ modules/api.js | 80 ++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + utility.js | 11 +++++++ views/sign_up.ejs | 27 +++++++++++---- 5 files changed, 112 insertions(+), 12 deletions(-) diff --git a/config-example.json b/config-example.json index 3ed6aa8..bb636e9 100644 --- a/config-example.json +++ b/config-example.json @@ -10,6 +10,11 @@ "dialect": "sqlite", "storage": "syzoj.db" }, + "register_mail": { + "enabled": true, + "address": "test@test.domain", + "key": "test" + }, "upload_dir": "uploads", "default": { "problem": { diff --git a/modules/api.js b/modules/api.js index 757102e..4bf61e7 100644 --- a/modules/api.js +++ b/modules/api.js @@ -54,6 +54,9 @@ app.post('/api/sign_up', async (req, res) => { res.setHeader('Content-Type', 'application/json'); let user = await User.fromName(req.body.username); if (user) throw 2008; + user = await User.findOne({ where: { email: req.body.email } }); + if (user) throw 2009; + // Because the salt is "syzoj2_xxx" and the "syzoj2_xxx" 's md5 is"59cb..." // the empty password 's md5 will equal "59cb.." @@ -62,20 +65,87 @@ app.post('/api/sign_up', async (req, res) => { if (!(req.body.email = req.body.email.trim())) throw 2006; if (!syzoj.utils.isValidUsername(req.body.username)) throw 2002; + if (syzoj.config.register_mail.enabled) { + let sendmail = Promise.promisify(require('sendmail')()); + let sendObj = { + username: req.body.username, + password: req.body.password, + email: req.body.email, + prevUrl: req.body.prevUrl, + r: Math.random() + }; + let encrypted = encodeURIComponent(syzoj.utils.encrypt(JSON.stringify(sendObj), syzoj.config.register_mail.key).toString('base64')); + let url = req.protocol + '://' + req.get('host') + syzoj.utils.makeUrl(['api', 'sign_up', encrypted]); + try { + await sendmail({ + from: syzoj.config.register_mail.address, + to: req.body.email, + type: 'text/html', + subject: `${req.body.username} 的 ${syzoj.config.title} 注册验证邮件`, + html: `

请点击该链接完成您在 ${syzoj.config.title} 的注册:${url}

如果您不是 ${req.body.username},请忽略此邮件。

` + }); + } catch (e) { + throw 2010 + } + + res.send(JSON.stringify({ error_code: 2 })); + } else { + user = await User.create({ + username: req.body.username, + password: req.body.password, + email: req.body.email + }); + await user.save(); + + req.session.user_id = user.id; + setLoginCookie(user.username, user.password, res); + + res.send(JSON.stringify({ error_code: 1 })); + } + } catch (e) { + syzoj.log(e); + res.send(JSON.stringify({ error_code: e })); + } +}); + +app.get('/api/sign_up/:token', async (req, res) => { + try { + let obj; + try { + let decrypted = syzoj.utils.decrypt(Buffer.from(req.params.token, 'base64'), syzoj.config.register_mail.key).toString(); + obj = JSON.parse(decrypted); + } catch (e) { + throw new ErrorMessage('无效的注册验证链接。'); + } + + let user = await User.fromName(obj.username); + if (user) throw new ErrorMessage('用户名已被占用。'); + user = await User.findOne({ where: { email: obj.email } }); + if (user) throw new ErrorMessage('邮件地址已被占用。'); + + // Because the salt is "syzoj2_xxx" and the "syzoj2_xxx" 's md5 is"59cb..." + // the empty password 's md5 will equal "59cb.." + let syzoj2_xxx_md5 = '59cb65ba6f9ad18de0dcd12d5ae11bd2'; + if (obj.password === syzoj2_xxx_md5) throw new ErrorMessage('密码不能为空。'); + if (!(obj.email = obj.email.trim())) throw new ErrorMessage('邮件地址不能为空。'); + if (!syzoj.utils.isValidUsername(obj.username)) throw new ErrorMessage('用户名不合法。'); + user = await User.create({ - username: req.body.username, - password: req.body.password, - email: req.body.email + username: obj.username, + password: obj.password, + email: obj.email }); await user.save(); req.session.user_id = user.id; setLoginCookie(user.username, user.password, res); - res.send(JSON.stringify({ error_code: 1 })); + res.redirect(obj.prevUrl || '/'); } catch (e) { syzoj.log(e); - res.send(JSON.stringify({ error_code: e })); + res.render('error', { + err: e + }); } }); diff --git a/package.json b/package.json index b44066a..014aa02 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "pygmentize-bundled-cached": "^1.1.0", "request": "^2.74.0", "request-promise": "^4.1.1", + "sendmail": "^1.1.1", "sequelize": "^3.24.3", "session-file-store": "^1.0.0", "sqlite3": "^3.1.4", diff --git a/utility.js b/utility.js index aadf487..a271be5 100644 --- a/utility.js +++ b/utility.js @@ -329,5 +329,16 @@ module.exports = { let s = JSON.stringify(key); if (!this.locks[s]) this.locks[s] = new AsyncLock(); return this.locks[s].acquire(s, cb); + }, + encrypt(buffer, password) { + if (typeof buffer === 'string') buffer = Buffer.from(buffer); + let crypto = require('crypto'); + let cipher = crypto.createCipher('aes-256-ctr', password); + return Buffer.concat([cipher.update(buffer), cipher.final()]); + }, + decrypt(buffer, password) { + let crypto = require('crypto'); + let decipher = crypto.createDecipher('aes-256-ctr', password); + return Buffer.concat([decipher.update(buffer), decipher.final()]); } }; diff --git a/views/sign_up.ejs b/views/sign_up.ejs index e0b05d9..3ce098d 100644 --- a/views/sign_up.ejs +++ b/views/sign_up.ejs @@ -14,10 +14,6 @@ -
@@ -37,10 +33,20 @@ function show_error(error) { $("#error_info").text(error); $("#error").show(); } + function success() { - alert("注册成功!"); + alert("注册成功!"); window.location.href = <%- JSON.stringify(req.query.url || '/') %>; } + +function mail_required() { + alert("注册确认邮件已经发送到您的邮箱的垃圾箱,点击邮件内的链接即可完成注册。"); + var s = $("#email").val(); + var mailWebsite = 'https://mail.' + s.substring(s.indexOf('@') + 1, s.length); + if (mailWebsite === 'https://mail.gmail.com') mailWebsite = 'https://mail.google.com'; + window.location.href = mailWebsite; +} + function submit() { if ($("#password1").val() != $("#password2").val()) { show_error("两次输入的密码不一致"); @@ -55,7 +61,8 @@ function submit() { data: { username: $("#username").val(), password: password, - email: $("#email").val() + email: $("#email").val(), + prevUrl: <%- JSON.stringify(req.query.url || '/') %> }, success: function(data) { error_code = data.error_code; @@ -79,11 +86,17 @@ function submit() { show_error("已经有人用过这个用户名了"); break; case 2009: - show_error("邀请码错误,请联系管理员索要"); + show_error("邮箱地址已被占用"); + break; + case 2010: + show_error("验证邮件发送失败"); break; case 1: success(); break; + case 2: + mail_required(); + break; default: show_error("未知错误"); break;