From fa05d98ea23e078ce9f19221a65b33a444ae8d2d Mon Sep 17 00:00:00 2001 From: Adam Rambousek Date: Wed, 2 May 2018 01:04:15 +0200 Subject: [PATCH] add password recovery by email --- data/lexonomy.sqlite.template | Bin 172032 -> 172032 bytes data/updates.js | 16 ++++++ website/lexonomy.js | 24 ++++++++- website/libs/screenful/screenful-forgotpwd.js | 29 +++++++++-- website/libs/screenful/screenful-loc-en.js | 8 ++- .../libs/screenful/screenful-recoverpwd.css | 18 +++++++ .../libs/screenful/screenful-recoverpwd.js | 43 ++++++++++++++++ website/ops.js | 48 +++++++++++++++++- website/package.json | 1 + website/siteconfig.json.template | 1 + website/views/forgotpwd.ejs | 3 +- website/views/recoverpwd.ejs | 35 +++++++++++++ 12 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 data/updates.js mode change 100755 => 100644 website/libs/screenful/screenful-loc-en.js create mode 100644 website/libs/screenful/screenful-recoverpwd.css create mode 100644 website/libs/screenful/screenful-recoverpwd.js create mode 100644 website/views/recoverpwd.ejs diff --git a/data/lexonomy.sqlite.template b/data/lexonomy.sqlite.template index f4a76d4a5bdfb55b44e5c5a7d245d3e948b16983..2afa38f4b98ab405e4a5d1144e2825a535ec1681 100644 GIT binary patch delta 313 zcmZoTz}0YoYl5`kGzJC+Eg%*KVp$;8pQvN3I*mb3yeRRK*>K_fLcF*8S@B($Waj57q$HN4mSpCp>L`>Jr>3|7d1yk&#!P<9-OdB_C>R8&vWZXj<&oJui6?(E oD@aISv*3jL{1X>QZ@R!9z$i1FLxEA9QK8*HfpNQm0@HH~cU8lYY|o!1uoIyS~?aS>Lp;%loDGp7*ETA9^o&SG~iYr=HI}Klgmc zQ}^UOGoE4hBlnwb!#(eM??BVaxAZZnj)+d9~$2%LI4|9)S12tKemj0$t4? zH2G zOU`=tsmD)e0C~Yo6p@f+rJVKa^>C^)4M@B)R;g-oVQY0ZMGu_=7?Sl5d5inAGIf zJun|X1IT5Qtw}O#M2*HZf;e>X^tsNL?3`GmrgLhtX6F=I1mw6G&vAOb8c#0Q{{zdO z(|{yRwum@JWmMh}Fq*Oq=>WI=c0q~{$|PBLn8jkircdd@LrOPtQ}jjrzz zhkabi#(|X$p4(&^w2v60I%l1j3S}0Rn3A0aJ>%@-t1Me$syjzb+oo8pTFWwwb#gZrjzx?)CjE18HVH$|9JvS%k9 z|FMc*t3e%;J=wSgh}QiqI~k?dr#L{C%~-a!2~j*|{kb|Ej8k-CGM1cVqw8GKMj|F9 z;)u&vor>sA0J6`F<@w51S<*5Xi8&e3aVHcuTZC%X0nz^=3&^S&)vGvmqiTKhcV;XU zvuTVmhMY_$(0|eRN)!h=Zbo?y3Dp{EKo_wjPfRis>-`KMBW6^qMJ1I{mg~0H=dBc? z?PH@Tv6(EHu`>!Dno!ls0;P${CRF<;oXyF&$!B9sT96cZPamEd2c%&#dZ}y_Y6QW;fLshC<_ zJC8@brlYB&HaQH;#qyBJ+T>WHzw@--^AC8jn?jhXX-9^T|;1?$37vV;=w4}`-6jE zXh<_Qk%lAz56eB`@Brvu)MP1L!6bb6W+Ssq^!AQYfaY*yPGEwP6+}TU7|7bbcVK%z z2|}a>WgV(WN;}`%*QM|w5av`x(Y4*Y&e<|Gx057wK~ZyAMJbPj!0435<6k+56D-tYLV^W49gd*m_(9b*<#}0r{ zIQ7J)kdvj2sft)|`>DdzxB6ss!c84AiOy>fetmR*-XwCPKNehy@vG z0+gw=tp~fYubiUhBY71{NK0Xnns-EUALyMzN(9N)oqu5DyD;+4`b1Ed1SxB%1+77B z5C^=mPS6K)P^**-)yn+#Sf)1sLgAc{gSduxv<0P#h$uxC>z#hkJ&@JPGw08pPoJJn zpIO}R1Hqy5n0XQIyq$(R?gb;WFsmA-iEAU0MLM+J{!QQZK@SLxW}VECyp!RqV#vCR zfj{O3Q8r_!xc-WfSw$2T9@-_mYohj;w0$nn7ux-?`=N(Bz!0~rs0Apm){w02h?FDa zTSAE}L60uT!|k{*(@Uww-t<8 diff --git a/data/updates.js b/data/updates.js new file mode 100644 index 00000000..f4958c6b --- /dev/null +++ b/data/updates.js @@ -0,0 +1,16 @@ +const path=require("path"); +const fs=require("fs-extra"); +const sqlite3 = require('sqlite3').verbose(); //https://www.npmjs.com/package/sqlite3 + +fs.readFile(path.join(__dirname, "../website/siteconfig.json"), "utf8", function(err, content){ + var siteconfig=JSON.parse(content); + var db=new sqlite3.Database(path.join(siteconfig.dataDir, "lexonomy.sqlite"), sqlite3.OPEN_READWRITE); + db.run("CREATE TABLE IF NOT EXISTS recovery_tokens (email text, requestAddress text, token text, expiration datetime, usedDate datetime, usedAddress text)", {}, function(err) { + if (err) { + return console.error(err.message); + } + console.log("Table recovery_tokens created."); + }); + db.close(); +}); + diff --git a/website/lexonomy.js b/website/lexonomy.js index a4781161..7850850b 100644 --- a/website/lexonomy.js +++ b/website/lexonomy.js @@ -19,6 +19,8 @@ const url=require("url"); const querystring=require("querystring"); const libxslt=require("libxslt"); //https://www.npmjs.com/package/libxslt const sqlite3 = require('sqlite3').verbose(); //https://www.npmjs.com/package/sqlite3 +const nodemailer = require('nodemailer'); +ops.mailtransporter = nodemailer.createTransport(siteconfig.mailconfig); const PORT=process.env.PORT||siteconfig.port||80; app.use(function (req, res, next) { @@ -87,7 +89,15 @@ app.get(siteconfig.rootPath+"signup/", function(req, res){ }); app.get(siteconfig.rootPath+"forgotpwd/", function(req, res){ ops.verifyLogin(req.cookies.email, req.cookies.sessionkey, function(user){ - res.render("forgotpwd.ejs", {user: user, email: siteconfig.admins[0], siteconfig: siteconfig}); + res.render("forgotpwd.ejs", {user: user, redirectUrl: siteconfig.baseUrl, siteconfig: siteconfig}); + }); +}); +app.get(siteconfig.rootPath+"recoverpwd/:token/", function(req, res){ + ops.verifyLogin(req.cookies.email, req.cookies.sessionkey, function(user){ + ops.verifyToken(req.params.token, function(valid){ + var tokenValid = valid; + res.render("recoverpwd.ejs", {user: user, redirectUrl: siteconfig.baseUrl, siteconfig: siteconfig, token: req.params.token, tokenValid: tokenValid}); + }); }); }); app.get(siteconfig.rootPath+"changepwd/", function(req, res){ @@ -130,6 +140,18 @@ app.post(siteconfig.rootPath+"changepwd.json", function(req, res){ } }); }); +app.post(siteconfig.rootPath+"forgotpwd.json", function(req, res){ + var remoteip = req.connection.remoteAddress.replace('::ffff:',''); + ops.sendToken(req.body.email, remoteip, req.body.mailSubject, req.body.mailText, function(success){ + res.json({success: success}); + }); +}); +app.post(siteconfig.rootPath+"recoverpwd.json", function(req, res){ + var remoteip = req.connection.remoteAddress.replace('::ffff:',''); + ops.resetPwd(req.body.token, req.body.password, remoteip, function(success){ + res.json({success: success}); + }); +}); //DOCS: app.get(siteconfig.rootPath+"docs/:docID/", function(req, res){ diff --git a/website/libs/screenful/screenful-forgotpwd.js b/website/libs/screenful/screenful-forgotpwd.js index 5cc585f5..4807b60a 100644 --- a/website/libs/screenful/screenful-forgotpwd.js +++ b/website/libs/screenful/screenful-forgotpwd.js @@ -1,9 +1,32 @@ Screenful.ForgotPwd={ start: function(){ Screenful.createEnvelope(true); - $("#envelope").html("
"); - $("#middlebox").append("
"+Screenful.Loc.forgotPwdEmail+"
"); - $("#middlebox").append(""); + $("#envelope").html("
"); + $("#middlebox .one").append("
"+Screenful.Loc.forgotPwdEmail+"
"); + $("#middlebox .one").append(""); + $("#middlebox .one").append("
"); + $("#middlebox .two").append("
"+Screenful.Loc.tokenSent+"
"); + $("#middlebox .two").append("
"); + + $("#middlebox div.field.email input").focus(); + $("#middlebox").on("submit", function(e){ + var email=$("#middlebox div.field.email input").val(); + if (email!="") Screenful.ForgotPwd.sendToken(email); + return false; + }); + + $("#middlebox button.return").on("click", function(e){ + window.location=Screenful.ForgotPwd.returnUrl; + }); + }, + + sendToken: function(email){ + $.ajax({url: Screenful.ForgotPwd.actionUrl, dataType: "json", method: "POST", data: {email: email, mailSubject: Screenful.Loc.recoverEmailSubject, mailText: Screenful.Loc.recoverEmailText}}).done(function(data){ + if(data.success) { + $("#middlebox .one").hide(); + $("#middlebox .two").show() + } + }); }, }; $(window).ready(Screenful.ForgotPwd.start); diff --git a/website/libs/screenful/screenful-loc-en.js b/website/libs/screenful/screenful-loc-en.js old mode 100755 new mode 100644 index f7119ad1..77290ae7 --- a/website/libs/screenful/screenful-loc-en.js +++ b/website/libs/screenful/screenful-loc-en.js @@ -38,8 +38,14 @@ Screenful.Loc={ changePwd: "Change your password", signup: "Get an account", forgotPwd: "Forgot your password?", + recoverPwd: "Get new password", + recoverEmailSubject: "Lexonomy, password recovery", + recoverEmailText: "Dear Lexonomy user,\nsomebody (hopefully you, from the address <%=remoteip%>) requested a new password for Lexonomy account <%=email%>. No changes have been made to your account yet.\n\nYou can reset your password by clicking the link below:\n\n <%=tokenurl%>\n\nFor security reason, this link is only valid until <%=expiredate%>.\n\nIf you did not request this password reset, please feel free to ignore this message.\n\nYours,\nLexonomy team", + recoverPwdMsg: "Please, enter your new password.", + invalidToken: "This recovery token is invalid, either it expired, or was already used to reset password. If you need to change your password, please request new recover token here.", signupEmail: "To get an account send an e-mail to", - forgotPwdEmail: "If you have forgotten your password send an e-mail to", + forgotPwdEmail: "If you have forgotten your password, enter your e-mail and we will send you information how to create a new one.", + tokenSent: "We have sent e-mail with further instructions to specified address.", newPassword: "New password", change: "Change", passwordChanged: "Your password has been changed.", diff --git a/website/libs/screenful/screenful-recoverpwd.css b/website/libs/screenful/screenful-recoverpwd.css new file mode 100644 index 00000000..b02a7e19 --- /dev/null +++ b/website/libs/screenful/screenful-recoverpwd.css @@ -0,0 +1,18 @@ +#middlebox {max-width: 600px; padding: 40px 30px; min-height: 100px; margin: 75px auto 50px auto; border: 1px solid rgb(38, 122, 181); background-color: #ffffff; border-radius: 4px; box-shadow: 0px 0px 4px #666666; } + +#middlebox div.field {margin-top: 30px;} +#middlebox div.field:first-child {margin-top: 0px;} +#middlebox div.field div.label {font-weight: bold; margin: 0px 0px 5px 0px; color: #333333;} +#middlebox div.field input.textbox {box-sizing: border-box; width: 100%; margin: 0px 0px 0px 0px; font: inherit; border-width: 0px; border-radius: 4px; background-color: #ffffff; box-shadow: inset 0px 0px 2px #666666; padding: 9px 8px; min-height: 1.3em; display: inline-block; vertical-align: middle;} +#middlebox div.field input.button {box-sizing: border-box; margin: 0px 0px 0px 0px; font: inherit; border-width: 0px; border-radius: 4px; background-color: #ffffff; box-shadow: 0px 0px 2px #666666; padding: 9px 30px; min-height: 1.3em; display: inline-block; vertical-align: middle; color: #267ab5; cursor: pointer;} +#middlebox div.field input.button:hover {color: #4698d1;} +#middlebox div.field button {box-sizing: border-box; margin: 0px 0px 0px 0px; font: inherit; border-width: 0px; border-radius: 4px; background-color: #ffffff; box-shadow: 0px 0px 2px #666666; padding: 9px 30px; min-height: 1.3em; display: inline-block; vertical-align: middle; color: #267ab5; cursor: pointer;} +#middlebox div.field button:hover {color: #4698d1;} + +#middlebox div.field.submit {text-align: center;} +#middlebox div.field.submit input.button {font-weight: bold;} +#middlebox div.field.submit button {font-weight: bold;} + +#middlebox div.error {background-color: #ffcdcc; color: #99004d; font-weight: bold; text-align: center; padding: 40px; margin: 30px -30px -40px -30px; text-shadow: 1px 1px 0px #eeeeee;} + +#middlebox div.two div.message {text-align: center; margin: 20px 0px 30px 0px;} diff --git a/website/libs/screenful/screenful-recoverpwd.js b/website/libs/screenful/screenful-recoverpwd.js new file mode 100644 index 00000000..bd434591 --- /dev/null +++ b/website/libs/screenful/screenful-recoverpwd.js @@ -0,0 +1,43 @@ +Screenful.RecoverPwd={ + start: function(){ + Screenful.createEnvelope(true); + $("#envelope").html("
"); + if (Screenful.RecoverPwd.tokenValid) { + $("#middlebox .one").append("
"+Screenful.Loc.recoverPwdMsg+"
"); + $("#middlebox .one").append("
"+Screenful.Loc.newPassword+"
"); + $("#middlebox .one").append("
"); + $("#middlebox .one").append(""); + $("#middlebox .two").append("
"+Screenful.Loc.passwordChanged+"
"); + $("#middlebox .two").append("
"); + } else { + $("#middlebox .one").append("
"+Screenful.Loc.invalidToken+"
"); + } + + $("#middlebox div.field.password input").focus(); + + $("#middlebox").on("submit", function(e){ + var password=$("#middlebox div.field.password input").val(); + if(password=="") { $("#middlebox .error").html(Screenful.Loc.passwordEmpty).show(); return false; } + if(password.length<6) { $("#middlebox .error").html(Screenful.Loc.passwordShort).show(); return false; } + if($.trim(password)!=password) { $("#middlebox .error").html(Screenful.Loc.passwordWhitespace).show(); return false; } + Screenful.RecoverPwd.go(password); + return false; + }); + + $("#middlebox button.return").on("click", function(e){ + window.location=Screenful.RecoverPwd.returnUrl; + }); + }, + + go: function(password){ + $.ajax({url: Screenful.RecoverPwd.actionUrl, dataType: "json", method: "POST", data: {password: password, token: Screenful.RecoverPwd.token}}).done(function(data){ + if(data.success) { + $("#middlebox .one").hide(); + $("#middlebox .two").show() + } + }); + }, + + +}; +$(window).ready(Screenful.RecoverPwd.start); diff --git a/website/ops.js b/website/ops.js index e23c65ca..37a94d7e 100644 --- a/website/ops.js +++ b/website/ops.js @@ -4,9 +4,11 @@ const xmldom=require("xmldom"); //https://www.npmjs.com/package/xmldom const sqlite3 = require('sqlite3').verbose(); //https://www.npmjs.com/package/sqlite3 const sha1 = require('sha1'); //https://www.npmjs.com/package/sha1 const markdown = require("markdown").markdown; //https://www.npmjs.com/package/markdown +const nodemailer = require('nodemailer'); module.exports={ siteconfig: {}, //populated by lexonomy.js on startup + mailtransporter: null, getDB: function(dictID, readonly){ var mode=(readonly ? sqlite3.OPEN_READONLY : sqlite3.OPEN_READWRITE) var db=new sqlite3.Database( @@ -699,6 +701,50 @@ module.exports={ callnext(true); }); }, + sendToken: function(email, remoteip, mailSubject, mailText, callnext){ + var db=new sqlite3.Database(path.join(module.exports.siteconfig.dataDir, "lexonomy.sqlite"), sqlite3.OPEN_READWRITE); + db.get("select email from users where email=$email", {$email: email}, function(err, row){ + if (row) { + var expireDate = (new Date()); expireDate.setHours(expireDate.getHours()+48); + expireDate = expireDate.toISOString(); + var token = sha1(sha1(Math.random())); + var tokenurl = module.exports.siteconfig.baseUrl + 'recoverpwd/' + token; + mailText = mailText + .replace('<%=remoteip%>',remoteip) + .replace('<%=email%>',email) + .replace('<%=expiredate%>',expireDate) + .replace('<%=tokenurl%>', tokenurl); + db.run("insert into recovery_tokens (email, requestAddress, token, expiration) values ($email, $remoteip, $token, $expire)", {$email: email, $expire: expireDate, $remoteip: remoteip, $token: token}, function(err, row){ + module.exports.mailtransporter.sendMail({from: 'xrambous@fi.muni.cz', to: email, subject: mailSubject, text: mailText}, (err, info) => {}); + db.close(); + }); + } + }); + callnext(true); + }, + verifyToken: function(token, callnext){ + var db=new sqlite3.Database(path.join(module.exports.siteconfig.dataDir, "lexonomy.sqlite"), sqlite3.OPEN_READWRITE); + db.get("select * from recovery_tokens where token=$token and expiration>=datetime('now') and usedDate is null", {$token: token}, function(err, row){ + db.close(); + if (!row) callnext(false); + else callnext(true); + }); + }, + resetPwd: function(token, password, remoteip, callnext){ + var db=new sqlite3.Database(path.join(module.exports.siteconfig.dataDir, "lexonomy.sqlite"), sqlite3.OPEN_READWRITE); + db.get("select * from recovery_tokens where token=$token and expiration>=datetime('now') and usedDate is null", {$token: token}, function(err, row){ + if (row) { + var email = row.email; + var hash = sha1(password); + db.run("update users set passwordHash=$hash where email=$email", {$hash: hash, $email: email}, function(err, row){ + db.run("update recovery_tokens set usedDate=datetime('now'), usedAddress=$remoteip where token=$token", {$remoteip: remoteip, $token: token}, function(err, row){ + db.close(); + callnext(true); + }); + }); + } + }); + }, verifyLogin: function(email, sessionkey, callnext){ var yesterday=(new Date()); yesterday.setHours(yesterday.getHours()-24); yesterday=yesterday.toISOString(); var db=new sqlite3.Database(path.join(module.exports.siteconfig.dataDir, "lexonomy.sqlite"), sqlite3.OPEN_READWRITE); @@ -975,4 +1021,4 @@ function generateDictID(){ return "z"+id; } -const prohibitedDictIDs=["login", "logout", "make", "signup", "forgotpwd", "changepwd", "users", "dicts", "oneclick"]; +const prohibitedDictIDs=["login", "logout", "make", "signup", "forgotpwd", "changepwd", "users", "dicts", "oneclick", "recoverpwd"]; diff --git a/website/package.json b/website/package.json index 93ede86d..1073b53e 100644 --- a/website/package.json +++ b/website/package.json @@ -7,6 +7,7 @@ "libxslt": "^0.7.0", "markdown": "^0.5.0", "multer": "^1.3.0", + "nodemailer": "^4.6.4", "sha1": "^1.1.1", "sqlite3": "^3.1.13", "xmldom": "^0.1.27" diff --git a/website/siteconfig.json.template b/website/siteconfig.json.template index 54bebf01..49334a79 100644 --- a/website/siteconfig.json.template +++ b/website/siteconfig.json.template @@ -8,6 +8,7 @@ "admins": ["root@localhost"], "trackingCode": "", "welcome": "Welcome to your Lexonomy installation.", + "mailconfig": {"host": "smtp.server.example", "port": 465,"secure": true}, "licences": { "cc-by-4.0": { "title": "Creative Commons Attribution 4.0 International", diff --git a/website/views/forgotpwd.ejs b/website/views/forgotpwd.ejs index fe983ce2..38bad30e 100644 --- a/website/views/forgotpwd.ejs +++ b/website/views/forgotpwd.ejs @@ -20,7 +20,8 @@ diff --git a/website/views/recoverpwd.ejs b/website/views/recoverpwd.ejs new file mode 100644 index 00000000..987412b9 --- /dev/null +++ b/website/views/recoverpwd.ejs @@ -0,0 +1,35 @@ + + + + + + Reset your password + + + + + + + + + + + + + + + + + +