* 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) {
|
function readyFileAreaWeb(callback) {
|
||||||
return require('./file_area_web.js').startup(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) {
|
function readyEventScheduler(callback) {
|
||||||
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
const EventSchedulerModule = require('./event_scheduler.js').EventSchedulerModule;
|
||||||
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
EventSchedulerModule.loadAndStart( (err, modInst) => {
|
||||||
|
|
|
@ -18,15 +18,26 @@ function ButtonView(options) {
|
||||||
|
|
||||||
util.inherits(ButtonView, TextView);
|
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) {
|
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||||
// allow space = submit
|
// allow space = submit
|
||||||
if(' ' === ch) {
|
if(' ' === ch) {
|
||||||
this.emit('action', 'accept');
|
this.emit('action', 'accept');
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
ButtonView.prototype.getData = function() {
|
ButtonView.prototype.getData = function() {
|
||||||
return null;
|
return this.submitData || null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -230,7 +230,26 @@ function getDefaultConfig() {
|
||||||
domain : 'another-fine-enigma-bbs.org',
|
domain : 'another-fine-enigma-bbs.org',
|
||||||
|
|
||||||
staticRoot : paths.join(__dirname, './../www'),
|
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 : {
|
http : {
|
||||||
enabled : false,
|
enabled : false,
|
||||||
port : 8080,
|
port : 8080,
|
||||||
|
@ -563,6 +582,12 @@ function getDefaultConfig() {
|
||||||
// - @execute:/path/to/something/executable.sh
|
// - @execute:/path/to/something/executable.sh
|
||||||
//
|
//
|
||||||
action : '@method:core/message_area.js:trimMessageAreasScheduledEvent',
|
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),
|
ExternalProcess : (reason, reasonCode) => new EnigError('External process error', -32005, reason, reasonCode),
|
||||||
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
MissingConfig : (reason, reasonCode) => new EnigError('Missing configuration', -32006, reason, reasonCode),
|
||||||
UnexpectedState : (reason, reasonCode) => new EnigError('Unexpected state', -32007, 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 = {
|
exports.ErrorReasons = {
|
||||||
|
|
|
@ -13,6 +13,7 @@ const StatLog = require('./stat_log.js');
|
||||||
const User = require('./user.js');
|
const User = require('./user.js');
|
||||||
const Log = require('./logger.js').log;
|
const Log = require('./logger.js').log;
|
||||||
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
const getConnectionByUserId = require('./client_connections.js').getConnectionByUserId;
|
||||||
|
const webServerPackageName = require('./servers/content/web.js').moduleInfo.packageName;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const hashids = require('hashids');
|
const hashids = require('hashids');
|
||||||
|
@ -23,8 +24,6 @@ const fs = require('fs');
|
||||||
const mimeTypes = require('mime-types');
|
const mimeTypes = require('mime-types');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
:TODO:
|
:TODO:
|
||||||
* Load temp download URLs @ startup & set expire timers via scheduler.
|
* Load temp download URLs @ startup & set expire timers via scheduler.
|
||||||
|
@ -51,9 +50,9 @@ class FileAreaWebAccess {
|
||||||
return self.load(callback);
|
return self.load(callback);
|
||||||
},
|
},
|
||||||
function addWebRoute(callback) {
|
function addWebRoute(callback) {
|
||||||
self.webServer = getServer(WEB_SERVER_PACKAGE_NAME);
|
self.webServer = getServer(webServerPackageName);
|
||||||
if(!self.webServer) {
|
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()) {
|
if(self.isEnabled()) {
|
||||||
|
@ -173,6 +172,9 @@ class FileAreaWebAccess {
|
||||||
|
|
||||||
buildTempDownloadLink(client, fileEntry, hashId) {
|
buildTempDownloadLink(client, fileEntry, hashId) {
|
||||||
hashId = hashId || this.getHashId(client, fileEntry);
|
hashId = hashId || this.getHashId(client, fileEntry);
|
||||||
|
|
||||||
|
return this.webServer.instance.buildUrl(`${Config.fileBase.web.path}${hashId}`);
|
||||||
|
/*
|
||||||
|
|
||||||
//
|
//
|
||||||
// Create a URL such as
|
// Create a URL such as
|
||||||
|
@ -200,6 +202,7 @@ class FileAreaWebAccess {
|
||||||
|
|
||||||
return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`;
|
return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||||
|
@ -246,7 +249,7 @@ class FileAreaWebAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
fileNotFound(resp) {
|
fileNotFound(resp) {
|
||||||
this.webServer.instance.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
return this.webServer.instance.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
routeWebRequestForFile(req, resp) {
|
routeWebRequestForFile(req, resp) {
|
||||||
|
|
|
@ -47,8 +47,7 @@ class PacketHeader {
|
||||||
point : 0,
|
point : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.packetVersion = version || '2+';
|
this.version = version || '2+';
|
||||||
|
|
||||||
this.origAddress = origAddr || EMPTY_ADDRESS;
|
this.origAddress = origAddr || EMPTY_ADDRESS;
|
||||||
this.destAddress = destAddr || EMPTY_ADDRESS;
|
this.destAddress = destAddr || EMPTY_ADDRESS;
|
||||||
this.created = createdMoment || moment();
|
this.created = createdMoment || moment();
|
||||||
|
@ -234,7 +233,7 @@ function Packet(options) {
|
||||||
//
|
//
|
||||||
// :TODO: adjust values based on version discovered
|
// :TODO: adjust values based on version discovered
|
||||||
if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
|
if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
|
||||||
packetHeader.packetVersion = '2.2';
|
packetHeader.version = '2.2';
|
||||||
|
|
||||||
// See FSC-0045
|
// See FSC-0045
|
||||||
packetHeader.origPoint = packetHeader.year;
|
packetHeader.origPoint = packetHeader.year;
|
||||||
|
@ -254,14 +253,14 @@ function Packet(options) {
|
||||||
0 != packetHeader.capWord &&
|
0 != packetHeader.capWord &&
|
||||||
packetHeader.capWord & 0x0001)
|
packetHeader.capWord & 0x0001)
|
||||||
{
|
{
|
||||||
packetHeader.packetVersion = '2+';
|
packetHeader.version = '2+';
|
||||||
|
|
||||||
// See FSC-0048
|
// See FSC-0048
|
||||||
if(-1 === packetHeader.origNet) {
|
if(-1 === packetHeader.origNet) {
|
||||||
packetHeader.origNet = packetHeader.auxNet;
|
packetHeader.origNet = packetHeader.auxNet;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
packetHeader.packetVersion = '2';
|
packetHeader.version = '2';
|
||||||
|
|
||||||
// :TODO: should fill bytes be 0?
|
// :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}`; }
|
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() {
|
isEnabled() {
|
||||||
return this.enableHttp || this.enableHttps;
|
return this.enableHttp || this.enableHttps;
|
||||||
}
|
}
|
||||||
|
@ -126,7 +157,7 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
routeRequest(req, resp) {
|
routeRequest(req, resp) {
|
||||||
const route = _.find(this.routes, r => r.matchesRequest(req) );
|
const route = _.find(this.routes, r => r.matchesRequest(req) );
|
||||||
return route ? route.handler(req, resp) : this.accessDenied(resp);
|
return route ? route.handler(req, resp) : this.accessDenied(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,6 +192,10 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
return this.respondWithError(resp, 401, 'Access denied.', 'Access Denied');
|
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) {
|
routeStaticFile(req, resp) {
|
||||||
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
||||||
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
|
const filePath = paths.join(Config.contentServers.web.staticRoot, fileName);
|
||||||
|
@ -168,7 +203,7 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
|
|
||||||
fs.stat(filePath, (err, stats) => {
|
fs.stat(filePath, (err, stats) => {
|
||||||
if(err) {
|
if(err) {
|
||||||
return self.respondWithError(resp, 404, 'File not found.', 'File Not Found');
|
return self.fileNotFound(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -181,4 +216,28 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||||
return readStream.pipe(resp);
|
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 removeClient = require('./client_connections.js').removeClient;
|
||||||
const ansiNormal = require('./ansi_term.js').normal;
|
const ansiNormal = require('./ansi_term.js').normal;
|
||||||
const userLogin = require('./user_login.js').userLogin;
|
const userLogin = require('./user_login.js').userLogin;
|
||||||
const messageArea = require('./message_area.js');
|
const messageArea = require('./message_area.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
|
||||||
exports.login = login;
|
exports.login = login;
|
||||||
exports.logoff = logoff;
|
exports.logoff = logoff;
|
||||||
exports.prevMenu = prevMenu;
|
exports.prevMenu = prevMenu;
|
||||||
exports.nextMenu = nextMenu;
|
exports.nextMenu = nextMenu;
|
||||||
exports.prevConf = prevConf;
|
exports.prevConf = prevConf;
|
||||||
exports.nextConf = nextConf;
|
exports.nextConf = nextConf;
|
||||||
exports.prevArea = prevArea;
|
exports.prevArea = prevArea;
|
||||||
exports.nextArea = nextArea;
|
exports.nextArea = nextArea;
|
||||||
|
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||||
|
|
||||||
function login(callingMenu, formData, extraArgs, cb) {
|
function login(callingMenu, formData, extraArgs, cb) {
|
||||||
|
|
||||||
|
@ -152,3 +153,21 @@ function nextArea(callingMenu, formData, extraArgs, cb) {
|
||||||
return reloadMenu(callingMenu, 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',
|
name : 'Last Callers',
|
||||||
desc : 'Last callers to the system',
|
desc : 'Last callers to the system',
|
||||||
author : 'NuSkooler',
|
author : 'NuSkooler',
|
||||||
packageName : 'codes.l33t.enigma.lastcallers' // :TODO: concept idea for mods
|
packageName : 'codes.l33t.enigma.lastcallers'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MciCodeIds = {
|
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
|
// File Base Related
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
|
@ -167,6 +185,15 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileBaseTagEntryPrompt: {
|
||||||
|
art: TAGFILE
|
||||||
|
mci: {
|
||||||
|
ET1: {
|
||||||
|
argName: tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
// Standard / Required
|
// Standard / Required
|
||||||
//
|
//
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "enigma-bbs",
|
"name": "enigma-bbs",
|
||||||
"version": "0.0.4-alpha",
|
"version": "0.0.5-alpha",
|
||||||
"description": "ENiGMA½ Bulletin Board System",
|
"description": "ENiGMA½ Bulletin Board System",
|
||||||
"author": "Bryan Ashby <bryan@l33t.codes>",
|
"author": "Bryan Ashby <bryan@l33t.codes>",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
@ -40,7 +40,8 @@
|
||||||
"sqlite3": "^3.1.1",
|
"sqlite3": "^3.1.1",
|
||||||
"ssh2": "^0.5.1",
|
"ssh2": "^0.5.1",
|
||||||
"temptmp" : "^1.0.0",
|
"temptmp" : "^1.0.0",
|
||||||
"sanitize-filename" : "^1.6.1"
|
"sanitize-filename" : "^1.6.1",
|
||||||
|
"nodemailer" : "^3.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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