* Bump version to 0.0.5-alpha
* Add email password reset support
This commit is contained in:
parent
97e19957ce
commit
f5899bc10f
|
@ -243,6 +243,10 @@ function initialize(cb) {
|
|||
function readyFileAreaWeb(callback) {
|
||||
return require('./file_area_web.js').startup(callback);
|
||||
},
|
||||
function readyPasswordReset(callback) {
|
||||
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
|
||||
return WebPasswordReset.startup(callback);
|
||||
},
|
||||
function readyEventScheduler(callback) {
|
||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
||||
|
|
|
@ -18,6 +18,16 @@ function ButtonView(options) {
|
|||
|
||||
util.inherits(ButtonView, TextView);
|
||||
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
if(this.isKeyMapped('accept', key.name) || ' ' === ch) {
|
||||
this.submitData = 'accept';
|
||||
this.emit('action', 'accept');
|
||||
delete this.submitData;
|
||||
} else {
|
||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
}
|
||||
};
|
||||
/*
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
// allow space = submit
|
||||
if(' ' === ch) {
|
||||
|
@ -26,7 +36,8 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
|
|||
|
||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
};
|
||||
*/
|
||||
|
||||
ButtonView.prototype.getData = function() {
|
||||
return null;
|
||||
return this.submitData || null;
|
||||
};
|
||||
|
|
|
@ -231,6 +231,25 @@ function getDefaultConfig() {
|
|||
|
||||
staticRoot : paths.join(__dirname, './../www'),
|
||||
|
||||
resetPassword : {
|
||||
//
|
||||
// The following templates have these variables available to them:
|
||||
//
|
||||
// * %BOARDNAME% : Name of BBS
|
||||
// * %USERNAME% : Username of whom to reset password
|
||||
// * %TOKEN% : Reset token
|
||||
// * %RESET_URL% : In case of email, the link to follow for reset. In case of landing page,
|
||||
// URL to POST submit reset form.
|
||||
|
||||
// templates for pw reset *email*
|
||||
resetPassEmailText : paths.join(__dirname, '../misc/reset_password_email.template.txt'), // plain text version
|
||||
resetPassEmailHtml : paths.join(__dirname, '../misc/reset_password_email.template.html'), // HTML version
|
||||
|
||||
// tempalte for pw reset *landing page*
|
||||
//
|
||||
resetPageTemplate : paths.join(__dirname, './../www/reset_password.template.html'),
|
||||
},
|
||||
|
||||
http : {
|
||||
enabled : false,
|
||||
port : 8080,
|
||||
|
@ -563,6 +582,12 @@ function getDefaultConfig() {
|
|||
// - @execute:/path/to/something/executable.sh
|
||||
//
|
||||
action : '@method:core/message_area.js:trimMessageAreasScheduledEvent',
|
||||
},
|
||||
|
||||
forgotPasswordMaintenance : {
|
||||
schedule : 'every 24 hours',
|
||||
action : '@method:core/web_password_reset.js:performMaintenanceTask',
|
||||
args : [ '24 hours' ] // items older than this will be removed
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const nodeMailer = require('nodemailer');
|
||||
|
||||
exports.sendMail = sendMail;
|
||||
|
||||
function sendMail(message, cb) {
|
||||
if(!_.has(Config, 'email.transport')) {
|
||||
return cb(Errors.MissingConfig('Email "email::transport" configuration missing'));
|
||||
}
|
||||
|
||||
message.from = message.from || Config.email.defaultFrom;
|
||||
|
||||
const transportOptions = Object.assign( {}, Config.email.transport, {
|
||||
logger : Log,
|
||||
});
|
||||
|
||||
const transport = nodeMailer.createTransport(transportOptions);
|
||||
|
||||
transport.sendMail(message, (err, info) => {
|
||||
return cb(err, info);
|
||||
});
|
||||
}
|
|
@ -30,6 +30,7 @@ exports.Errors = {
|
|||
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
||||
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
||||
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, reason, reasonCode),
|
||||
MissingParam : (reason, reasonCode) => new EnigError('Missing paramater(s)', -32008, reason, reasonCode),
|
||||
};
|
||||
|
||||
exports.ErrorReasons = {
|
||||
|
|
|
@ -13,6 +13,7 @@ const StatLog = require('./stat_log.js');
|
|||
const User = require('./user.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
||||
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||
|
||||
// deps
|
||||
const hashids = require('hashids');
|
||||
|
@ -23,8 +24,6 @@ const fs = require('fs');
|
|||
const mimeTypes = require('mime-types');
|
||||
const _ = require('lodash');
|
||||
|
||||
const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server';
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
* Load temp download URLs @ startup & set expire timers via scheduler.
|
||||
|
@ -51,9 +50,9 @@ class FileAreaWebAccess {
|
|||
return self.load(callback);
|
||||
},
|
||||
function addWebRoute(callback) {
|
||||
self.webServer = getServer(WEB_SERVER_PACKAGE_NAME);
|
||||
self.webServer = getServer(webServerPackageName);
|
||||
if(!self.webServer) {
|
||||
return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`));
|
||||
return callback(Errors.DoesNotExist(`Server with package name "${webServerPackageName}" does not exist`));
|
||||
}
|
||||
|
||||
if(self.isEnabled()) {
|
||||
|
@ -174,6 +173,9 @@ class FileAreaWebAccess {
|
|||
buildTempDownloadLink(client, fileEntry, hashId) {
|
||||
hashId = hashId || this.getHashId(client, fileEntry);
|
||||
|
||||
return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
|
||||
/*
|
||||
|
||||
//
|
||||
// Create a URL such as
|
||||
// https://l33t.codes:44512/f/qFdxyZr
|
||||
|
@ -200,6 +202,7 @@ class FileAreaWebAccess {
|
|||
|
||||
return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||
|
@ -246,7 +249,7 @@ class FileAreaWebAccess {
|
|||
}
|
||||
|
||||
fileNotFound(resp) {
|
||||
this.webServer.instance.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||
return this.webServer.instance.fileNotFound(resp);
|
||||
}
|
||||
|
||||
routeWebRequestForFile(req, resp) {
|
||||
|
|
|
@ -47,8 +47,7 @@ class PacketHeader {
|
|||
point : 0,
|
||||
};
|
||||
|
||||
this.packetVersion = version || '2+';
|
||||
|
||||
this.version = version || '2+';
|
||||
this.origAddress = origAddr || EMPTY_ADDRESS;
|
||||
this.destAddress = destAddr || EMPTY_ADDRESS;
|
||||
this.created = createdMoment || moment();
|
||||
|
@ -234,7 +233,7 @@ function Packet(options) {
|
|||
//
|
||||
// :TODO: adjust values based on version discovered
|
||||
if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
|
||||
packetHeader.packetVersion = '2.2';
|
||||
packetHeader.version = '2.2';
|
||||
|
||||
// See FSC-0045
|
||||
packetHeader.origPoint = packetHeader.year;
|
||||
|
@ -254,14 +253,14 @@ function Packet(options) {
|
|||
0 != packetHeader.capWord &&
|
||||
packetHeader.capWord & 0x0001)
|
||||
{
|
||||
packetHeader.packetVersion = '2+';
|
||||
packetHeader.version = '2+';
|
||||
|
||||
// See FSC-0048
|
||||
if(-1 === packetHeader.origNet) {
|
||||
packetHeader.origNet = packetHeader.auxNet;
|
||||
}
|
||||
} else {
|
||||
packetHeader.packetVersion = '2';
|
||||
packetHeader.version = '2';
|
||||
|
||||
// :TODO: should fill bytes be 0?
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ class Route {
|
|||
);
|
||||
}
|
||||
|
||||
matchesRequest(req) { return req.method === this.method && this.pathRegExp.test(req.url); }
|
||||
matchesRequest(req) {
|
||||
return req.method === this.method && this.pathRegExp.test(req.url);
|
||||
}
|
||||
|
||||
getRouteKey() { return `${this.method}:${this.path}`; }
|
||||
}
|
||||
|
@ -67,6 +69,35 @@ exports.getModule = class WebServerModule extends ServerModule {
|
|||
}
|
||||
}
|
||||
|
||||
buildUrl(pathAndQuery) {
|
||||
//
|
||||
// Create a URL such as
|
||||
// https://l33t.codes:44512/ + |pathAndQuery|
|
||||
//
|
||||
// Prefer HTTPS over HTTP. Be explicit about the port
|
||||
// only if non-standard. Allow users to override full prefix in config.
|
||||
//
|
||||
if(_.isString(Config.contentServers.web.overrideUrlPrefix)) {
|
||||
return `${Config.contentServers.web.overrideUrlPrefix}${pathAndQuery}`;
|
||||
}
|
||||
|
||||
let schema;
|
||||
let port;
|
||||
if(Config.contentServers.web.https.enabled) {
|
||||
schema = 'https://';
|
||||
port = (443 === Config.contentServers.web.https.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.https.port}`;
|
||||
} else {
|
||||
schema = 'http://';
|
||||
port = (80 === Config.contentServers.web.http.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.http.port}`;
|
||||
}
|
||||
|
||||
return `${schema}${Config.contentServers.web.domain}${port}${pathAndQuery}`;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.enableHttp || this.enableHttps;
|
||||
}
|
||||
|
@ -161,6 +192,10 @@ exports.getModule = class WebServerModule extends ServerModule {
|
|||
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
|
||||
}
|
||||
|
||||
fileNotFound(resp) {
|
||||
return this.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||
}
|
||||
|
||||
routeStaticFile(req, resp) {
|
||||
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
||||
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
|
||||
|
@ -168,7 +203,7 @@ exports.getModule = class WebServerModule extends ServerModule {
|
|||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return self.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
||||
return self.fileNotFound(resp);
|
||||
}
|
||||
|
||||
const headers = {
|
||||
|
@ -181,4 +216,28 @@ exports.getModule = class WebServerModule extends ServerModule {
|
|||
return readStream.pipe(resp);
|
||||
});
|
||||
}
|
||||
|
||||
routeTemplateFilePage(templatePath, preprocessCallback, resp) {
|
||||
const self = this;
|
||||
|
||||
fs.readFile(templatePath, 'utf8', (err, templateData) => {
|
||||
if(err) {
|
||||
return self.fileNotFound(resp);
|
||||
}
|
||||
|
||||
preprocessCallback(templateData, (err, finalPage, contentType) => {
|
||||
if(err || !finalPage) {
|
||||
return self.respondWithError(resp, 500, 'Internal Server Error.', 'Internal Server Error');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type' : contentType || mimeTypes.contentType('.html'),
|
||||
'Content-Length' : finalPage.length,
|
||||
};
|
||||
|
||||
resp.writeHead(200, headers);
|
||||
return resp.end(finalPage);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,20 +5,21 @@
|
|||
const removeClient = require('./client_connections.js').removeClient;
|
||||
const ansiNormal = require('./ansi_term.js').normal;
|
||||
const userLogin = require('./user_login.js').userLogin;
|
||||
const messageArea = require('./message_area.js');
|
||||
const messageArea = require('./message_area.js');
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
exports.login = login;
|
||||
exports.logoff = logoff;
|
||||
exports.prevMenu = prevMenu;
|
||||
exports.nextMenu = nextMenu;
|
||||
exports.prevConf = prevConf;
|
||||
exports.nextConf = nextConf;
|
||||
exports.prevArea = prevArea;
|
||||
exports.nextArea = nextArea;
|
||||
exports.login = login;
|
||||
exports.logoff = logoff;
|
||||
exports.prevMenu = prevMenu;
|
||||
exports.nextMenu = nextMenu;
|
||||
exports.prevConf = prevConf;
|
||||
exports.nextConf = nextConf;
|
||||
exports.prevArea = prevArea;
|
||||
exports.nextArea = nextArea;
|
||||
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||
|
||||
function login(callingMenu, formData, extraArgs, cb) {
|
||||
|
||||
|
@ -152,3 +153,21 @@ function nextArea(callingMenu, formData, extraArgs, cb) {
|
|||
return reloadMenu(callingMenu, cb);
|
||||
});
|
||||
}
|
||||
|
||||
function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
||||
const username = formData.value.username || callingMenu.client.user.username;
|
||||
|
||||
const WebPasswordReset = require('./web_password_reset.js').WebPasswordReset;
|
||||
|
||||
WebPasswordReset.sendForgotPasswordEmail(username, err => {
|
||||
if(err) {
|
||||
callingMenu.client.log.warn( { err : err.message }, 'Failed sending forgot password email');
|
||||
}
|
||||
|
||||
if(extraArgs.next) {
|
||||
return callingMenu.gotoMenu(extraArgs.next, cb);
|
||||
}
|
||||
|
||||
return logoff(callingMenu, formData, extraArgs, cb);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
const getServer = require('./listening_server.js').getServer;
|
||||
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||
const User = require('./user.js');
|
||||
const userDb = require('./database.js').dbs.user;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const Log = require('./logger.js').log;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const _ = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const querystring = require('querystring');
|
||||
|
||||
const PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT =
|
||||
`%USERNAME%:
|
||||
a password reset has been requested for your account on %BOARDNAME%.
|
||||
|
||||
* If this was not you, please ignore this email.
|
||||
* Otherwise, follow this link: %RESET_URL%
|
||||
`;
|
||||
|
||||
function getWebServer() {
|
||||
return getServer(webServerPackageName);
|
||||
}
|
||||
|
||||
class WebPasswordReset {
|
||||
|
||||
static startup(cb) {
|
||||
WebPasswordReset.registerRoutes( err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
static sendForgotPasswordEmail(username, cb) {
|
||||
const webServer = getServer(webServerPackageName);
|
||||
if(!webServer || !webServer.instance.isEnabled()) {
|
||||
return cb(Errors.General('Web server is not enabled'));
|
||||
}
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
function getEmailAddress(callback) {
|
||||
if(!username) {
|
||||
return callback(Errors.MissingParam('Missing "username"'));
|
||||
}
|
||||
|
||||
User.getUserIdAndName(username, (err, userId) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
User.getUser(userId, (err, user) => {
|
||||
if(err || !user.properties.email_address) {
|
||||
return callback(Errors.DoesNotExist('No email address associated with this user'));
|
||||
}
|
||||
|
||||
return callback(null, user);
|
||||
});
|
||||
});
|
||||
},
|
||||
function generateAndStoreResetToken(user, callback) {
|
||||
//
|
||||
// Reset "token" is simply HEX encoded cryptographically generated bytes
|
||||
//
|
||||
crypto.randomBytes(256, (err, token) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
token = token.toString('hex');
|
||||
|
||||
const newProperties = {
|
||||
email_password_reset_token : token,
|
||||
email_password_reset_token_ts : getISOTimestampString(),
|
||||
};
|
||||
|
||||
// we simply place the reset token in the user's properties
|
||||
user.persistProperties(newProperties, err => {
|
||||
return callback(err, user);
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
function getEmailTemplates(user, callback) {
|
||||
fs.readFile(Config.contentServers.web.resetPassword.resetPassEmailText, 'utf8', (err, textTemplate) => {
|
||||
if(err) {
|
||||
textTemplate = PW_RESET_EMAIL_TEXT_TEMPLATE_DEFAULT;
|
||||
}
|
||||
|
||||
fs.readFile(Config.contentServers.web.resetPassword.resetPassEmailHtml, 'utf8', (err, htmlTemplate) => {
|
||||
return callback(null, user, textTemplate, htmlTemplate);
|
||||
});
|
||||
});
|
||||
},
|
||||
function buildAndSendEmail(user, textTemplate, htmlTemplate, callback) {
|
||||
const sendMail = require('./email.js').sendMail;
|
||||
|
||||
const resetUrl = webServer.instance.buildUrl(`/reset_password?token=${user.properties.email_password_reset_token}`);
|
||||
|
||||
function replaceTokens(s) {
|
||||
return s
|
||||
.replace(/%BOARDNAME%/g, Config.general.boardName)
|
||||
.replace(/%USERNAME%/g, user.username)
|
||||
.replace(/%TOKEN%/g, user.properties.email_password_reset_token)
|
||||
.replace(/%RESET_URL%/g, resetUrl)
|
||||
;
|
||||
}
|
||||
|
||||
textTemplate = replaceTokens(textTemplate);
|
||||
if(htmlTemplate) {
|
||||
htmlTemplate = replaceTokens(htmlTemplate);
|
||||
}
|
||||
|
||||
const message = {
|
||||
to : `${user.properties.display_name||user.username} <${user.properties.email_address}>`,
|
||||
// from will be filled in
|
||||
subject : 'Forgot Password',
|
||||
text : textTemplate,
|
||||
html : htmlTemplate,
|
||||
};
|
||||
|
||||
sendMail(message, (err, info) => {
|
||||
// :TODO: Log me!
|
||||
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static scheduleEvents(cb) {
|
||||
// :TODO: schedule ~daily cleanup task
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
static registerRoutes(cb) {
|
||||
const webServer = getWebServer();
|
||||
if(!webServer) {
|
||||
return cb(null); // no webserver enabled
|
||||
}
|
||||
|
||||
if(!webServer.instance.isEnabled()) {
|
||||
return cb(null); // no error, but we're not serving web stuff
|
||||
}
|
||||
|
||||
[
|
||||
{
|
||||
// this is the page displayed to user when they GET it
|
||||
method : 'GET',
|
||||
path : '^\\/reset_password\\?token\\=[a-f0-9]+$', // Config.contentServers.web.forgotPasswordPageTemplate
|
||||
handler : WebPasswordReset.routeResetPasswordGet,
|
||||
},
|
||||
// POST handler for performing the actual reset
|
||||
{
|
||||
method : 'POST',
|
||||
path : '^\\/reset_password$',
|
||||
handler : WebPasswordReset.routeResetPasswordPost,
|
||||
}
|
||||
].forEach(r => {
|
||||
webServer.instance.addRoute(r);
|
||||
});
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
|
||||
static fileNotFound(webServer, resp) {
|
||||
return webServer.instance.fileNotFound(resp);
|
||||
}
|
||||
|
||||
static accessDenied(webServer, resp) {
|
||||
return webServer.instance.accessDenied(resp);
|
||||
}
|
||||
|
||||
static getUserByToken(token, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
function validateToken(callback) {
|
||||
User.getUserIdsWithProperty('email_password_reset_token', token, (err, userIds) => {
|
||||
if(userIds && userIds.length === 1) {
|
||||
return callback(null, userIds[0]);
|
||||
}
|
||||
|
||||
return callback(Errors.Invalid('Invalid password reset token'));
|
||||
});
|
||||
},
|
||||
function getUser(userId, callback) {
|
||||
User.getUser(userId, (err, user) => {
|
||||
return callback(null, user);
|
||||
});
|
||||
},
|
||||
],
|
||||
(err, user) => {
|
||||
return cb(err, user);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static routeResetPasswordGet(req, resp) {
|
||||
const webServer = getWebServer(); // must be valid, we just got a req!
|
||||
|
||||
const urlParts = url.parse(req.url, true);
|
||||
const token = urlParts.query && urlParts.query.token;
|
||||
|
||||
if(!token) {
|
||||
return WebPasswordReset.accessDenied(webServer, resp);
|
||||
}
|
||||
|
||||
WebPasswordReset.getUserByToken(token, (err, user) => {
|
||||
if(err) {
|
||||
// assume it's expired
|
||||
return webServer.instance.respondWithError(resp, 410, 'Invalid or expired reset link.', 'Expired Link');
|
||||
}
|
||||
|
||||
const postResetUrl = webServer.instance.buildUrl('/reset_password');
|
||||
|
||||
return webServer.instance.routeTemplateFilePage(
|
||||
Config.contentServers.web.resetPassword.resetPageTemplate,
|
||||
(templateData, preprocessFinished) => {
|
||||
|
||||
const finalPage = templateData
|
||||
.replace(/%BOARDNAME%/g, Config.general.boardName)
|
||||
.replace(/%USERNAME%/g, user.username)
|
||||
.replace(/%TOKEN%/g, token)
|
||||
.replace(/%RESET_URL%/g, postResetUrl)
|
||||
;
|
||||
|
||||
return preprocessFinished(null, finalPage);
|
||||
},
|
||||
resp
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static routeResetPasswordPost(req, resp) {
|
||||
const webServer = getWebServer(); // must be valid, we just got a req!
|
||||
|
||||
let bodyData = '';
|
||||
req.on('data', data => {
|
||||
bodyData += data;
|
||||
});
|
||||
|
||||
function badRequest() {
|
||||
return webServer.instance.respondWithError(resp, 400, 'Bad Request.', 'Bad Request');
|
||||
}
|
||||
|
||||
req.on('end', () => {
|
||||
const formData = querystring.parse(bodyData);
|
||||
|
||||
if(!formData.token || !formData.password || !formData.confirm_password ||
|
||||
formData.password !== formData.confirm_password ||
|
||||
formData.password.length < Config.users.passwordMin || formData.password.length > Config.users.passwordMax)
|
||||
{
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
WebPasswordReset.getUserByToken(formData.token, (err, user) => {
|
||||
if(err) {
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
user.setNewAuthCredentials(formData.password, err => {
|
||||
if(err) {
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
// delete assoc properties - no need to wait for completion
|
||||
user.removeProperty('email_password_reset_token');
|
||||
user.removeProperty('email_password_reset_token_ts');
|
||||
|
||||
resp.writeHead(200);
|
||||
return resp.end('Password changed successfully');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function performMaintenanceTask(args, cb) {
|
||||
|
||||
const forgotPassExpireTime = args[0] || '24 hours';
|
||||
|
||||
// remove all reset token associated properties older than |forgotPassExpireTime|
|
||||
userDb.run(
|
||||
`DELETE FROM user_property
|
||||
WHERE user_id IN (
|
||||
SELECT user_id
|
||||
FROM user_property
|
||||
WHERE prop_name = "email_password_reset_token_ts"
|
||||
AND DATETIME("now") >= DATETIME(prop_value, "+${forgotPassExpireTime}")
|
||||
) AND prop_name IN ("email_password_reset_token_ts", "email_password_reset_token");`,
|
||||
err => {
|
||||
if(err) {
|
||||
Log.warn( { error : err.message }, 'Failed deleting old email reset tokens');
|
||||
}
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
exports.WebPasswordReset = WebPasswordReset;
|
||||
exports.performMaintenanceTask = performMaintenanceTask;
|
|
@ -0,0 +1,7 @@
|
|||
<b>%USERNAME%</b>:<br>
|
||||
<br>
|
||||
A password reset request has been generated for your account on <b>%BOARDNAME% BBS</b>:<br>
|
||||
<br>
|
||||
If you did not make this request, <u>you may safely ignore this email</u>. Otherwise, <a href="%RESET_URL%">please follow this link</a> to reset your password. If that does not work, you can copy the link below:<br>
|
||||
<br>
|
||||
%RESET_URL%<br>
|
|
@ -0,0 +1,7 @@
|
|||
%USERNAME%:
|
||||
|
||||
A password reset request has been generated for your account on %BOARDNAME% BBS:
|
||||
|
||||
If you did not make this request, you may safely ignore this email. Otherwise, please follow the link below to reset your password:
|
||||
|
||||
%RESET_URL%
|
|
@ -27,7 +27,7 @@ exports.moduleInfo = {
|
|||
name : 'Last Callers',
|
||||
desc : 'Last callers to the system',
|
||||
author : 'NuSkooler',
|
||||
packageName : 'codes.l33t.enigma.lastcallers' // :TODO: concept idea for mods
|
||||
packageName : 'codes.l33t.enigma.lastcallers'
|
||||
};
|
||||
|
||||
const MciCodeIds = {
|
||||
|
|
|
@ -133,6 +133,24 @@
|
|||
}
|
||||
},
|
||||
|
||||
forgotPasswordPrompt: {
|
||||
art: FORGOTPW
|
||||
mci: {
|
||||
ET1: {
|
||||
argName: username
|
||||
maxLength: @config:users.usernameMax
|
||||
width: 32
|
||||
focus: true
|
||||
}
|
||||
}
|
||||
actionKeys: [
|
||||
{
|
||||
keys: [ "escape" ]
|
||||
action: @systemMethod:prevMenu
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// File Base Related
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
@ -167,6 +185,15 @@
|
|||
]
|
||||
}
|
||||
|
||||
fileBaseTagEntryPrompt: {
|
||||
art: TAGFILE
|
||||
mci: {
|
||||
ET1: {
|
||||
argName: tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Standard / Required
|
||||
//
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "enigma-bbs",
|
||||
"version": "0.0.4-alpha",
|
||||
"version": "0.0.5-alpha",
|
||||
"description": "ENiGMA½ Bulletin Board System",
|
||||
"author": "Bryan Ashby <bryan@l33t.codes>",
|
||||
"license": "BSD-2-Clause",
|
||||
|
@ -40,7 +40,8 @@
|
|||
"sqlite3": "^3.1.1",
|
||||
"ssh2": "^0.5.1",
|
||||
"temptmp" : "^1.0.0",
|
||||
"sanitize-filename" : "^1.6.1"
|
||||
"sanitize-filename" : "^1.6.1",
|
||||
"nodemailer" : "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Password Reset — ENiGMA½ BBS</title>
|
||||
<meta name="description" content="Reset your password">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.getElementById('password').onchange = validatePassword;
|
||||
document.getElementById('confirm_password').onchange = validatePassword;
|
||||
}
|
||||
|
||||
function validatePassword() {
|
||||
var pw = document.getElementById('password');
|
||||
var confirm = document.getElementById('confirm_password');
|
||||
|
||||
if(pw.value !== confirm.value) {
|
||||
confirm.setCustomValidity('Passwords must match!');
|
||||
} else {
|
||||
confirm.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<form action="%RESET_URL%" method="post">
|
||||
<legend>Password Reset</legend>
|
||||
<input type="password" placeholder="Password" id="password" name="password" required>
|
||||
<input type="password" placeholder="Confirm Password" id="confirm_password" name="confirm_password" required>
|
||||
<input type="hidden" value="%TOKEN%" name="token">
|
||||
<button type="submit">Confirm</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue