Continue work on password reset/recovery

This commit is contained in:
calzoneman 2014-01-24 11:20:16 -06:00
parent 65ef082a64
commit 63ed9c7883
3 changed files with 220 additions and 81 deletions

View File

@ -75,7 +75,7 @@ module.exports.query = function (query, sub, callback) {
} }
} }
}); });
}; };
/** /**
* Dummy function to be used as a callback when none is provided * Dummy function to be used as a callback when none is provided
@ -99,7 +99,7 @@ module.exports.initGlobalTables = function () {
"PRIMARY KEY (`ip`)) " + "PRIMARY KEY (`ip`)) " +
"CHARACTER SET utf8", "CHARACTER SET utf8",
fail("global_bans")); fail("global_bans"));
query("CREATE TABLE IF NOT EXISTS `password_reset` (" + query("CREATE TABLE IF NOT EXISTS `password_reset` (" +
"`ip` VARCHAR(39) NOT NULL," + "`ip` VARCHAR(39) NOT NULL," +
"`name` VARCHAR(20) NOT NULL," + "`name` VARCHAR(20) NOT NULL," +
@ -235,6 +235,27 @@ module.exports.globalUnbanIP = function (ip, callback) {
/* password recovery */ /* password recovery */
module.exports.addPasswordReset = function (data, cb) {
if (typeof cb !== "function") {
cb = blackHole;
}
var ip = data.ip || "";
var name = data.name;
var email = data.email;
var hash = data.hash;
var expire = data.expire;
if (!name || !hash) {
cb("Internal error: Must provide name and hash to insert a new password reset", null);
return;
}
module.exports.query("INSERT INTO `password_reset` (`ip`, `name`, `email`, `hash`, `expire`) " +
"VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE ip=?, hash=?, email=?, expire=?",
[ip, name, email, hash, expire, ip, hash, email, expire], cb);
};
/* /*
module.exports.genPasswordReset = function (ip, name, email, callback) { module.exports.genPasswordReset = function (ip, name, email, callback) {
if(typeof callback !== "function") if(typeof callback !== "function")
@ -506,7 +527,7 @@ module.exports.getIPs = function (name, callback) {
if(!err) { if(!err) {
ips = res.map(function (row) { return row.ip; }); ips = res.map(function (row) { return row.ip; });
} }
callback(err, ips); callback(err, ips);
}); });
}; };

View File

@ -1,3 +1,5 @@
var crypto = require("crypto");
/* /*
Set prototype- simple wrapper around JS objects to Set prototype- simple wrapper around JS objects to
manipulate them like a set manipulate them like a set
@ -194,5 +196,11 @@ module.exports = {
} }
}, },
Set: Set Set: Set,
sha1: function (data) {
var shasum = crypto.createHash("sha1");
shasum.update(data);
return shasum.digest("hex");
}
}; };

View File

@ -4,12 +4,13 @@
* @author Calvin Montgomery <cyzon@cyzon.us> * @author Calvin Montgomery <cyzon@cyzon.us>
*/ */
var webserver = require('./webserver'); var webserver = require("./webserver");
var logRequest = webserver.logRequest; var logRequest = webserver.logRequest;
var sendJade = require('./jade').sendJade; var sendJade = require("./jade").sendJade;
var Logger = require('../logger'); var Logger = require("../logger");
var db = require('../database'); var db = require("../database");
var $util = require('../utilities'); var $util = require("../utilities");
var Config = require("../config");
/** /**
* Handles a GET request for /account/edit * Handles a GET request for /account/edit
@ -22,25 +23,25 @@ function handleAccountEditPage(req, res) {
logRequest(req); logRequest(req);
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} }
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName loginName: loginName
}); });
} }
/** /**
* Handles a POST request to edit a user's account * Handles a POST request to edit a user"s account
*/ */
function handleAccountEdit(req, res) { function handleAccountEdit(req, res) {
logRequest(req); logRequest(req);
var action = req.body.action; var action = req.body.action;
switch(action) { switch(action) {
case 'change_password': case "change_password":
handleChangePassword(req, res); handleChangePassword(req, res);
break; break;
case 'change_email': case "change_email":
handleChangeEmail(req, res); handleChangeEmail(req, res);
break; break;
default: default:
@ -50,7 +51,7 @@ function handleAccountEdit(req, res) {
} }
/** /**
* Handles a request to change the user's password * Handles a request to change the user"s password
*/ */
function handleChangePassword(req, res) { function handleChangePassword(req, res) {
var name = req.body.name; var name = req.body.name;
@ -58,21 +59,21 @@ function handleChangePassword(req, res) {
var newpassword = req.body.newpassword; var newpassword = req.body.newpassword;
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} }
if (typeof name !== 'string' || if (typeof name !== "string" ||
typeof oldpassword !== 'string' || typeof oldpassword !== "string" ||
typeof newpassword !== 'string') { typeof newpassword !== "string") {
res.send(400); res.send(400);
return; return;
} }
if (newpassword.length === 0) { if (newpassword.length === 0) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: 'New password must not be empty' errorMessage: "New password must not be empty"
}); });
return; return;
} }
@ -81,7 +82,7 @@ function handleChangePassword(req, res) {
db.users.verifyLogin(name, oldpassword, function (err, user) { db.users.verifyLogin(name, oldpassword, function (err, user) {
if (err) { if (err) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: err errorMessage: err
@ -91,25 +92,25 @@ function handleChangePassword(req, res) {
db.users.setPassword(name, newpassword, function (err, dbres) { db.users.setPassword(name, newpassword, function (err, dbres) {
if (err) { if (err) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: err errorMessage: err
}); });
return; return;
} }
Logger.eventlog(webserver.ipForRequest(req) + ' changed password for ' + name); Logger.eventlog(webserver.ipForRequest(req) + " changed password for " + name);
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
successMessage: 'Password changed.' successMessage: "Password changed."
}); });
}); });
}); });
} }
/** /**
* Handles a request to change the user's email * Handles a request to change the user"s email
*/ */
function handleChangeEmail(req, res) { function handleChangeEmail(req, res) {
var name = req.body.name; var name = req.body.name;
@ -117,28 +118,28 @@ function handleChangeEmail(req, res) {
var email = req.body.email; var email = req.body.email;
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} }
if (typeof name !== 'string' || if (typeof name !== "string" ||
typeof password !== 'string' || typeof password !== "string" ||
typeof email !== 'string') { typeof email !== "string") {
res.send(400); res.send(400);
return; return;
} }
if (!$util.isValidEmail(email)) { if (!$util.isValidEmail(email)) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: 'Invalid email address' errorMessage: "Invalid email address"
}); });
return; return;
} }
db.users.verifyLogin(name, password, function (err, user) { db.users.verifyLogin(name, password, function (err, user) {
if (err) { if (err) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: err errorMessage: err
@ -148,19 +149,20 @@ function handleChangeEmail(req, res) {
db.users.setEmail(name, email, function (err, dbres) { db.users.setEmail(name, email, function (err, dbres) {
if (err) { if (err) {
sendJade(res, 'account-edit', { sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
errorMessage: err errorMessage: err
}); });
return; return;
} }
Logger.eventlog(webserver.ipForRequest(req) + ' changed email for ' + name + // TODO event log
' to ' + email); Logger.syslog.log(webserver.ipForRequest(req) + " changed email for " + name +
sendJade(res, 'account-edit', { " to " + email);
sendJade(res, "account-edit", {
loggedIn: loginName !== false, loggedIn: loginName !== false,
loginName: loginName, loginName: loginName,
successMessage: 'Email address changed.' successMessage: "Email address changed."
}); });
}); });
}); });
@ -177,19 +179,19 @@ function handleAccountChannelPage(req, res) {
logRequest(req); logRequest(req);
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} }
if (loginName) { if (loginName) {
db.channels.listUserChannels(loginName, function (err, channels) { db.channels.listUserChannels(loginName, function (err, channels) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
channels: channels channels: channels
}); });
}); });
} else { } else {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: false, loggedIn: false,
channels: [], channels: [],
}); });
@ -197,16 +199,16 @@ function handleAccountChannelPage(req, res) {
} }
/** /**
* Handles a POST request to modify a user's channels * Handles a POST request to modify a user"s channels
*/ */
function handleAccountChannel(req, res) { function handleAccountChannel(req, res) {
logRequest(req); logRequest(req);
var action = req.body.action; var action = req.body.action;
switch(action) { switch(action) {
case 'new_channel': case "new_channel":
handleNewChannel(req, res); handleNewChannel(req, res);
break; break;
case 'delete_channel': case "delete_channel":
handleDeleteChannel(req, res); handleDeleteChannel(req, res);
break; break;
default: default:
@ -222,16 +224,16 @@ function handleNewChannel(req, res) {
logRequest(req); logRequest(req);
var name = req.body.name; var name = req.body.name;
if (typeof name !== 'string') { if (typeof name !== "string") {
res.send(400); res.send(400);
return; return;
} }
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} else { } else {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: false, loggedIn: false,
channels: [] channels: []
}); });
@ -239,7 +241,7 @@ function handleNewChannel(req, res) {
} }
db.users.verifyAuth(req.cookies.auth, function (err, user) { db.users.verifyAuth(req.cookies.auth, function (err, user) {
if (err) { if (err) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: false, loggedIn: false,
channels: [], channels: [],
newChannelError: err newChannelError: err
@ -249,7 +251,7 @@ function handleNewChannel(req, res) {
db.channels.register(name, user.name, function (err, channel) { db.channels.register(name, user.name, function (err, channel) {
db.channels.listUserChannels(loginName, function (err2, channels) { db.channels.listUserChannels(loginName, function (err2, channels) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
channels: err2 ? [] : channels, channels: err2 ? [] : channels,
@ -267,16 +269,16 @@ function handleDeleteChannel(req, res) {
logRequest(req); logRequest(req);
var name = req.body.name; var name = req.body.name;
if (typeof name !== 'string') { if (typeof name !== "string") {
res.send(400); res.send(400);
return; return;
} }
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} else { } else {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: false, loggedIn: false,
channels: [], channels: [],
}); });
@ -284,7 +286,7 @@ function handleDeleteChannel(req, res) {
} }
db.users.verifyAuth(req.cookies.auth, function (err, user) { db.users.verifyAuth(req.cookies.auth, function (err, user) {
if (err) { if (err) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: false, loggedIn: false,
channels: [], channels: [],
deleteChannelError: err deleteChannelError: err
@ -295,18 +297,18 @@ function handleDeleteChannel(req, res) {
db.channels.lookup(name, function (err, channel) { db.channels.lookup(name, function (err, channel) {
if (channel.owner !== user.name && user.global_rank < 255) { if (channel.owner !== user.name && user.global_rank < 255) {
db.channels.listUserChannels(loginName, function (err2, channels) { db.channels.listUserChannels(loginName, function (err2, channels) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
channels: err2 ? [] : channels, channels: err2 ? [] : channels,
deleteChannelError: 'You do not have permission to delete this channel' deleteChannelError: "You do not have permission to delete this channel"
}); });
}); });
return; return;
} }
db.channels.drop(name, function (err) { db.channels.drop(name, function (err) {
db.channels.listUserChannels(loginName, function (err2, channels) { db.channels.listUserChannels(loginName, function (err2, channels) {
sendJade(res, 'account-channels', { sendJade(res, "account-channels", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
channels: err2 ? [] : channels, channels: err2 ? [] : channels,
@ -330,29 +332,29 @@ function handleAccountProfilePage(req, res) {
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} else { } else {
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: false, loggedIn: false,
profileImage: '', profileImage: "",
profileText: '' profileText: ""
}); });
return; return;
} }
db.users.getProfile(loginName, function (err, profile) { db.users.getProfile(loginName, function (err, profile) {
if (err) { if (err) {
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
profileError: err, profileError: err,
profileImage: '', profileImage: "",
profileText: '' profileText: ""
}); });
return; return;
} }
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: true, loggedIn: true,
loginName: loginName, loginName: loginName,
profileImage: profile.image, profileImage: profile.image,
@ -370,9 +372,9 @@ function handleAccountProfile(req, res) {
var loginName = false; var loginName = false;
if (req.cookies.auth) { if (req.cookies.auth) {
loginName = req.cookies.auth.split(':')[0]; loginName = req.cookies.auth.split(":")[0];
} else { } else {
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: false, loggedIn: false,
profileImage: "", profileImage: "",
profileText: "", profileText: "",
@ -386,7 +388,7 @@ function handleAccountProfile(req, res) {
db.users.verifyAuth(req.cookies.auth, function (err, user) { db.users.verifyAuth(req.cookies.auth, function (err, user) {
if (err) { if (err) {
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: false, loggedIn: false,
profileImage: "", profileImage: "",
profileText: "", profileText: "",
@ -397,7 +399,7 @@ function handleAccountProfile(req, res) {
db.users.setProfile(user.name, { image: image, text: text }, function (err) { db.users.setProfile(user.name, { image: image, text: text }, function (err) {
if (err) { if (err) {
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: true, loggedIn: true,
loginName: user.name, loginName: user.name,
profileImage: "", profileImage: "",
@ -407,7 +409,7 @@ function handleAccountProfile(req, res) {
return; return;
} }
sendJade(res, 'account-profile', { sendJade(res, "account-profile", {
loggedIn: true, loggedIn: true,
loginName: user.name, loginName: user.name,
profileImage: image, profileImage: image,
@ -436,7 +438,7 @@ function handlePasswordResetPage(req, res) {
} }
/** /**
* Handles a POST request to reset a user's password * Handles a POST request to reset a user"s password
*/ */
function handlePasswordReset(req, res) { function handlePasswordReset(req, res) {
logRequest(req); logRequest(req);
@ -485,25 +487,133 @@ function handlePasswordReset(req, res) {
return; return;
} }
sendJade(res, "account-passwordreset", { var hash = $util.sha1($util.randomSalt(64));
reset: true, // 24-hour expiration
resetEmail: user.email, var expire = Date.now() + 86400000;
resetErr: false var ip = webserver.ipForRequest(req);
db.addPasswordReset({
ip: ip,
name: name,
email: email,
hash: hash,
expire: expire
}, function (err, dbres) {
if (err) {
sendJade(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: err
});
return;
}
if (!Config.get("mail.enabled")) {
sendJade(res, "account-passwordreset", {
reset: false,
resetEmail: email,
resetErr: "This server does not have mail support enabled. Please " +
"contact an administrator for assistance."
});
return;
}
var msg = "A password reset request was issued for your " +
"account `"+ name + "` on " + Config.get("http.domain") +
". This request is valid for 24 hours. If you did "+
"not initiate this, there is no need to take action."+
" To reset your password, copy and paste the " +
"following link into your browser: " +
Config.get("http.domain") + "/passwordrecover/"+hash;
var mail = {
from: "CyTube Services <" + Config.get("mail.from") + ">",
to: email,
subject: "Password reset request",
text: msg
};
Config.get("nodemailer").sendMail(mail, function (err, response) {
if (err) {
Logger.errlog.log("mail fail: " + err);
sendJade(res, "account-passwordreset", {
reset: false,
resetEmail: user.email,
resetErr: "Sending reset email failed. Please contact an " +
"administrator for assistance."
});
} else {
sendJade(res, "account-passwordreset", {
reset: true,
resetEmail: user.email,
resetErr: false
});
}
});
}); });
}); });
} }
/**
* Handles a request for /passwordreceover/<hash>
*/
function handlePasswordRecover(req, res) {
var hash = req.query.hash;
if (typeof hash !== "string") {
res.send(400);
return;
}
var ip = webserver.ipForRequest(req);
db.lookupPasswordReset(hash, function (err, row) {
if (err) {
sendJade(req, "account-passwordrecover", {
recovered: false,
recoverErr: err,
loginName: false
});
return;
}
if (row.ip && row.ip !== ip) {
sendJade(req, "account-passwordrecover", {
recovered: false,
recoverErr: "Your IP address does not match the address " +
"used to submit the reset request. For your " +
"security, only the IP which initiates the reset " +
"may reclaim an account.",
loginName: false
});
return;
}
if (Date.now() >= row.expire) {
sendJade(req, "account-passwordrecover", {
recovered: false,
recoverErr: "This password recovery link has expired. Password " +
"recovery links are valid only for 24 hours after " +
"submission.",
loginName: false
});
return;
}
// TODO actual reset
});
}
module.exports = { module.exports = {
/** /**
* Initialize the module * Initialize the module
*/ */
init: function (app) { init: function (app) {
app.get('/account/edit', handleAccountEditPage); app.get("/account/edit", handleAccountEditPage);
app.post('/account/edit', handleAccountEdit); app.post("/account/edit", handleAccountEdit);
app.get('/account/channels', handleAccountChannelPage); app.get("/account/channels", handleAccountChannelPage);
app.post('/account/channels', handleAccountChannel); app.post("/account/channels", handleAccountChannel);
app.get('/account/profile', handleAccountProfilePage); app.get("/account/profile", handleAccountProfilePage);
app.post('/account/profile', handleAccountProfile); app.post("/account/profile", handleAccountProfile);
app.get("/account/passwordreset", handlePasswordResetPage); app.get("/account/passwordreset", handlePasswordResetPage);
app.post("/account/passwordreset", handlePasswordReset); app.post("/account/passwordreset", handlePasswordReset);
} }