Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit - still needs to be connected to back end repository t…

…o actually reset account
  • Loading branch information...
commit e5fd5e44de232abc88d8a2dc0073173c60723e10 0 parents
@cliftonc authored
11 .project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>npm-users</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ </buildSpec>
+ <natures>
+ </natures>
+</projectDescription>
33 lib/emailer.js
@@ -0,0 +1,33 @@
+/**
+ * Simple emailer
+ */
+var email = require('nodemailer');
+
+exports.Mailer = {
+
+ sendMail: function(params,callback) {
+
+ // CHANGE THESE
+ email.SMTP = {
+ host: "smtp.gmail.com",
+ port: 465,
+ ssl: true,
+ use_authentication: true,
+ user: "<USER>@gmail.com",
+ pass: "<PASSWORD"
+ }
+
+ email.send_mail({
+ to : params.email,
+ sender : params.helpEmail,
+ subject : "NPM Account Reset for " + params.username,
+ body: "This email has been sent as you (@" + params.username + ") requested the reset of your NPM user account. \r\n\r\n Please click on the following link, or paste this into your " +
+ "browser to complete the process: http://" + params.baseUrl + "/confirm/" + params.tokenId + "\r\n\r\n" +
+ "If you received this in error, please contact: " + params.helpEmail
+ },
+ function(err, result){
+ callback(err,result);
+ });
+
+ }
+}
70 lib/token.registry.js
@@ -0,0 +1,70 @@
+/**
+ * TokenRegistry class - in memory register of reset tokens
+ * Best to have a cron job that restarts it every 48 hours
+ */
+var uuid = require('./uuid.core').UUID;
+
+exports.TokenRegistry = {
+
+ _currentTokens: {},
+
+ createToken: function(username, email, state, expires) {
+ var token = uuid.generate();
+ this._currentTokens[token] = { username: username, email: email, expires: expires, state: state };
+ return token;
+ },
+ removeToken: function(token) {
+ if (token in this._currentTokens)
+ delete this._currentTokens[token];
+ },
+ setState: function(token, state) {
+ if (token in this._currentTokens) {
+ this._currentTokens[token].state = state;
+ }
+ },
+ getState: function(token) {
+ if (token in this._currentTokens) {
+ return this._currentTokens[token].state;
+ }
+ },
+ getCurrent: function() {
+ var ret = {};
+ for (var idx in this._currentTokens) {
+ ret[idx] = ({ username: this._currentTokens[idx].username, email: this._currentTokens[idx].email, expires: this._currentTokens[idx].expires, state: this._currentTokens[idx].state });
+ }
+ return ret;
+ },
+ countTokens: function() {
+ var count = 0;
+ for (var idx in this._currentTokens) {
+ count++;
+ }
+ return count;
+ },
+ hasExpired: function(token) {
+ var currentDate = new Date();
+ if(token.expires < currentDate) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ getToken: function(tokenId,response) {
+
+ if (tokenId in this._currentTokens) {
+ var token = this._currentTokens[tokenId];
+ if(this.hasExpired(token)) {
+ // Take this opportunity to clean it up
+ delete this._currentTokens[tokenId];
+ response({type:'expired',message:'That token has expired.'},null);
+ } else {
+ response(null, token);
+ }
+
+ } else {
+ response({type:'invalid',message:'Token with ID ' + tokenId + ' not found.'},null);
+ }
+
+ }
+
+};
297 lib/uuid.core.js
@@ -0,0 +1,297 @@
+/*
+ The MIT License: Copyright (c) 2010 LiosK.
+*/
+
+exports.UUID = UUID;
+
+/**
+ * UUID.js: The RFC-compliant UUID generator for JavaScript.
+ *
+ * @fileOverview
+ * @author LiosK
+ * @version 3.1
+ * @license The MIT License: Copyright (c) 2010 LiosK.
+ */
+
+// Core Component {{{
+
+/** @constructor */
+function UUID() {}
+
+/**
+ * The simplest function to get an UUID string.
+ * @returns {string} A version 4 UUID string.
+ */
+UUID.generate = function() {
+ var rand = UUID._getRandomInt, hex = UUID._hexAligner;
+ return hex(rand(32), 8) // time_low
+ + "-"
+ + hex(rand(16), 4) // time_mid
+ + "-"
+ + hex(0x4000 | rand(12), 4) // time_hi_and_version
+ + "-"
+ + hex(0x8000 | rand(14), 4) // clock_seq_hi_and_reserved clock_seq_low
+ + "-"
+ + hex(rand(48), 12); // node
+};
+
+/**
+ * Returns an unsigned x-bit random integer.
+ * @param {int} x A positive integer ranging from 0 to 53, inclusive.
+ * @returns {int} An unsigned x-bit random integer (0 <= f(x) < 2^x).
+ */
+UUID._getRandomInt = function(x) {
+ if (x < 0) return NaN;
+ if (x <= 30) return (0 | Math.random() * (1 << x));
+ if (x <= 53) return (0 | Math.random() * (1 << 30))
+ + (0 | Math.random() * (1 << x - 30)) * (1 << 30);
+ return NaN;
+};
+
+/**
+ * Returns a function that converts an integer to a zero-filled string.
+ * @param {int} radix
+ * @returns {function(num&#44; length)}
+ */
+UUID._getIntAligner = function(radix) {
+ return function(num, length) {
+ var hex = num.toString(radix), i = length - hex.length, z = "0";
+ for (; i > 0; i >>>= 1, z += z) { if (i & 1) { hex = z + hex; } }
+ return hex;
+ };
+};
+
+UUID._hexAligner = UUID._getIntAligner(16);
+
+// }}}
+
+// UUID Object Component {{{
+
+/**
+ * Names of each UUID field.
+ * @type string[]
+ * @constant
+ * @since 3.0
+ */
+UUID.FIELD_NAMES = ["timeLow", "timeMid", "timeHiAndVersion",
+ "clockSeqHiAndReserved", "clockSeqLow", "node"];
+
+/**
+ * Sizes of each UUID field.
+ * @type int[]
+ * @constant
+ * @since 3.0
+ */
+UUID.FIELD_SIZES = [32, 16, 16, 8, 8, 48];
+
+/**
+ * Generates a version 4 {@link UUID}.
+ * @returns {UUID} A version 4 {@link UUID} object.
+ * @since 3.0
+ */
+UUID.genV4 = function() {
+ var rand = UUID._getRandomInt;
+ return new UUID()._init(rand(32), rand(16), // time_low time_mid
+ 0x4000 | rand(12), // time_hi_and_version
+ 0x80 | rand(6), // clock_seq_hi_and_reserved
+ rand(8), rand(48)); // clock_seq_low node
+};
+
+/**
+ * Converts hexadecimal UUID string to an {@link UUID} object.
+ * @param {string} strId UUID hexadecimal string representation ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
+ * @returns {UUID} {@link UUID} object or null.
+ * @since 3.0
+ */
+UUID.parse = function(strId) {
+ var r, p = /^(?:urn:uuid:)?([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})$/i;
+ if (r = p.exec(strId)) {
+ return new UUID()._init(parseInt(r[1], 16), parseInt(r[2], 16),
+ parseInt(r[3], 16), parseInt(r[4], 16),
+ parseInt(r[5], 16), parseInt(r[6], 16));
+ } else {
+ return null;
+ }
+};
+
+/**
+ * Initializes {@link UUID} object.
+ * @param {uint32} [timeLow=0] time_low field (octet 0-3).
+ * @param {uint16} [timeMid=0] time_mid field (octet 4-5).
+ * @param {uint16} [timeHiAndVersion=0] time_hi_and_version field (octet 6-7).
+ * @param {uint8} [clockSeqHiAndReserved=0] clock_seq_hi_and_reserved field (octet 8).
+ * @param {uint8} [clockSeqLow=0] clock_seq_low field (octet 9).
+ * @param {uint48} [node=0] node field (octet 10-15).
+ * @returns {UUID} this.
+ */
+UUID.prototype._init = function() {
+ var names = UUID.FIELD_NAMES, sizes = UUID.FIELD_SIZES;
+ var bin = UUID._binAligner, hex = UUID._hexAligner;
+
+ /**
+ * List of UUID field values (as integer values).
+ * @type int[]
+ */
+ this.intFields = new Array(6);
+
+ /**
+ * List of UUID field values (as binary bit string values).
+ * @type string[]
+ */
+ this.bitFields = new Array(6);
+
+ /**
+ * List of UUID field values (as hexadecimal string values).
+ * @type string[]
+ */
+ this.hexFields = new Array(6);
+
+ for (var i = 0; i < 6; i++) {
+ var intValue = parseInt(arguments[i] || 0);
+ this.intFields[i] = this.intFields[names[i]] = intValue;
+ this.bitFields[i] = this.bitFields[names[i]] = bin(intValue, sizes[i]);
+ this.hexFields[i] = this.hexFields[names[i]] = hex(intValue, sizes[i] / 4);
+ }
+
+ /**
+ * UUID version number defined in RFC 4122.
+ * @type int
+ */
+ this.version = (this.intFields.timeHiAndVersion >> 12) & 0xF;
+
+ /**
+ * 128-bit binary bit string representation.
+ * @type string
+ */
+ this.bitString = this.bitFields.join("");
+
+ /**
+ * UUID hexadecimal string representation ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
+ * @type string
+ */
+ this.hexString = this.hexFields[0] + "-" + this.hexFields[1] + "-" + this.hexFields[2]
+ + "-" + this.hexFields[3] + this.hexFields[4] + "-" + this.hexFields[5];
+
+ /**
+ * UUID string representation as a URN ("urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").
+ * @type string
+ */
+ this.urn = "urn:uuid:" + this.hexString;
+
+ return this;
+};
+
+UUID._binAligner = UUID._getIntAligner(2);
+
+/**
+ * Returns UUID string representation.
+ * @returns {string} {@link UUID#hexString}.
+ */
+UUID.prototype.toString = function() { return this.hexString; };
+
+/**
+ * Tests if two {@link UUID} objects are equal.
+ * @param {UUID} uuid
+ * @returns {bool} True if two {@link UUID} objects are equal.
+ */
+UUID.prototype.equals = function(uuid) {
+ if (!(uuid instanceof UUID)) { return false; }
+ for (var i = 0; i < 6; i++) {
+ if (this.intFields[i] !== uuid.intFields[i]) { return false; }
+ }
+ return true;
+};
+
+// }}}
+
+// UUID Version 1 Component {{{
+
+/**
+ * Generates a version 1 {@link UUID}.
+ * @returns {UUID} A version 1 {@link UUID} object.
+ * @since 3.0
+ */
+UUID.genV1 = function() {
+ var now = new Date().getTime(), st = UUID._state;
+ if (now != st.timestamp) {
+ if (now < st.timestamp) { st.sequence++; }
+ st.timestamp = now;
+ st.tick = UUID._getRandomInt(4);
+ } else if (Math.random() < UUID._tsRatio && st.tick < 9984) {
+ // advance the timestamp fraction at a probability
+ // to compensate for the low timestamp resolution
+ st.tick += 1 + UUID._getRandomInt(4);
+ } else {
+ st.sequence++;
+ }
+
+ // format time fields
+ var tf = UUID._getTimeFieldValues(st.timestamp);
+ var tl = tf.low + st.tick;
+ var thav = (tf.hi & 0xFFF) | 0x1000; // set version '0001'
+
+ // format clock sequence
+ st.sequence &= 0x3FFF;
+ var cshar = (st.sequence >>> 8) | 0x80; // set variant '10'
+ var csl = st.sequence & 0xFF;
+
+ return new UUID()._init(tl, tf.mid, thav, cshar, csl, st.node);
+};
+
+/**
+ * Re-initializes version 1 UUID state.
+ * @since 3.0
+ */
+UUID.resetState = function() {
+ UUID._state = new UUID._state.constructor();
+};
+
+/**
+ * Probability to advance the timestamp fraction: the ratio of tick movements to sequence increments.
+ * @type float
+ */
+UUID._tsRatio = 1 / 4;
+
+/**
+ * Persistent state for UUID version 1.
+ * @type UUIDState
+ */
+UUID._state = new function UUIDState() {
+ var rand = UUID._getRandomInt;
+ this.timestamp = 0;
+ this.sequence = rand(14);
+ this.node = (rand(8) | 1) * 0x10000000000 + rand(40); // set multicast bit '1'
+ this.tick = rand(4); // timestamp fraction smaller than a millisecond
+};
+
+/**
+ * @param {Date|int} time ECMAScript Date Object or milliseconds from 1970-01-01.
+ * @returns {object}
+ */
+UUID._getTimeFieldValues = function(time) {
+ var ts = time - Date.UTC(1582, 9, 15);
+ var hm = ((ts / 0x100000000) * 10000) & 0xFFFFFFF;
+ return { low: ((ts & 0xFFFFFFF) * 10000) % 0x100000000,
+ mid: hm & 0xFFFF, hi: hm >>> 16, timestamp: ts };
+};
+
+// }}}
+
+// Backward Compatibility Component {{{
+
+/**
+ * Reinstalls {@link UUID.generate} method to emulate the interface of UUID.js version 2.x.
+ * @since 3.1
+ * @deprecated Version 2.x. compatible interface is not recommended.
+ */
+UUID.makeBackwardCompatible = function() {
+ var f = UUID.generate;
+ UUID.generate = function(o) {
+ return (o && o.version == 1) ? UUID.genV1().hexString : f.call(UUID);
+ };
+ UUID.makeBackwardCompatible = function() {};
+};
+
+// }}}
+
+// vim: et ts=2 sw=2 fdm=marker fmr&
81 lib/validator.mixin.js
@@ -0,0 +1,81 @@
+/*
+ * This binds the node-validator library to the req object so that
+ * the validation / sanitization methods can be called on parameter
+ * names rather than the actual strings.
+ *
+ * 1. Be sure to include `req.mixinParams()` as middleware to merge
+ * query string, body and named parameters into `req.params`
+ *
+ * 2. To validate parameters, use `req.check(param_name, [err_message])`
+ * e.g. req.check('param1').len(1, 6).isInt();
+ * e.g. req.checkHeader('referer').contains('mydomain.com');
+ *
+ * Each call to `check()` will throw an exception by default. To
+ * specify a custom err handler, use `req.onValidationError(errback)`
+ * where errback receives a parameter containing the error message
+ *
+ * 3. To sanitize parameters, use `req.sanitize(param_name)`
+ * e.g. req.sanitize('large_text').xss();
+ * e.g. req.sanitize('param2').toInt();
+ *
+ * 4. Done! Access your validated and sanitized paramaters through the
+ * `req.params` object
+ */
+
+var http = require('http'),
+ Validator = require('validator').Validator,
+ Filter = require('validator').Filter;
+
+var validator = new Validator();
+
+http.IncomingMessage.prototype.mixinParams = function() {
+ this.params = this.params || {};
+ this.query = this.query || {};
+ this.body = this.body || {};
+
+ //Merge params from the query string
+ for (var i in this.query) {
+ if (typeof this.params[i] === 'undefined') {
+ this.params[i] = this.query[i];
+ }
+ }
+
+ //Merge params from the request body
+ for (var i in this.body) {
+ if (typeof this.params[i] === 'undefined') {
+ this.params[i] = this.body[i];
+ }
+ }
+}
+
+http.IncomingMessage.prototype.check = function(param, fail_msg) {
+ return validator.check(this.params[param], fail_msg);
+}
+
+http.IncomingMessage.prototype.checkHeader = function(param, fail_msg) {
+ var to_check;
+ if (header === 'referrer' || header === 'referer') {
+ to_check = this.headers['referer'];
+ } else {
+ to_check = this.headers[header];
+ }
+ return validator.check(to_check || '', fail_msg);
+}
+
+http.IncomingMessage.prototype.onValidationError = function(errback) {
+ validator.error = errback;
+}
+
+http.IncomingMessage.prototype.filter = function(param) {
+ var self = this;
+ var filter = new Filter();
+ filter.modify = function(str) {
+ this.str = str;
+ self.params[param] = str; //Replace the param with the filtered version
+ }
+ return filter.sanitize(this.params[param]);
+}
+
+//Create some aliases - might help with code readability
+http.IncomingMessage.prototype.sanitize = http.IncomingMessage.prototype.filter;
+http.IncomingMessage.prototype.assert = http.IncomingMessage.prototype.check;
21 package.json
@@ -0,0 +1,21 @@
+{
+ "name": "npm-users",
+ "description": "NPM User Reset",
+ "version": "0.1.0",
+ "homepage": "http://cliftonc.github.com/npm-users",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/cliftonc/npm-users"
+ },
+ "author": "Clifton Cunningham <clifton.cunningham@gmail.com> (cliftoncunningham.co.uk)",
+ "engines" : { "node" : "0.4 || 0.5" },
+ "dependencies" : {
+ "express" : ">=2.0.0beta3",
+ "connect" : ">=1.0.3",
+ "ejs" : ">=0.3.1",
+ "validator" : ">=0.1.7",
+ "nodemailer" : ">=0.1.8",
+ "expresso" : ">=0.7.3",
+ "should" : ">=0.0.4"
+ }
+}
281 public/css/style.css
@@ -0,0 +1,281 @@
+/* FEEL FREE TO CHANGE ANY OF THIS! */
+
+/*--- Reset ---*/
+* { margin: 0; padding: 0; }
+
+html, body { height: 100%; min-height: 100%; background-color: #000000; }
+
+/*--- Layout ---*/
+body {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5em;
+ color: #0d0d0d;
+ background-color: #fff;
+}
+
+#container {
+ position: relative;
+ padding: 10px 20%;
+}
+
+/*--- Basics ---*/
+h1, h2, h3, h4, h5, h6 {
+ font-weight:normal;
+ color:#111;
+ line-height: 1;
+ margin: 1.5em 0 0.5em 0;
+}
+h1 { font-size: 1.6em; }
+h2, h5 { font-size: 1.3em; color: #666; }
+h3, h6 { font-size: 1.2em; color: #00a8e6; }
+h4 { font-size: 1.1em; }
+h5 { font-size: 1.1em; }
+h6 { font-size: 1em; }
+p { margin-bottom: 1em; }
+strong { font-weight: bold; }
+em { font-style: italic; }
+a { text-decoration: none; color: #333; }
+a, h1 a, h2 a { text-decoration: none; }
+a:hover { color: #232323; }
+a:visited:hover { color: #232323; }
+a img { border: none; }
+
+/*--- Lists ---*/
+li ul, li ol { margin: 0; }
+ul, ol { margin: 0 0 1em 2.75em; }
+dt, dd {
+ font-style: italic;
+ margin: .5em 0;
+}
+dt {
+ font-weight: bold;
+}
+dd {
+ margin-left: 1em;
+}
+/*--- Header ---*/
+#header h1 {
+ margin: .1em 0;
+ font-size: 2.2em;
+}
+#header h2 {
+ width: 70%;
+ margin: .5em .5em 2em 0;
+ color: #666;
+ font-size: 1.3em;
+ line-height: 22px;
+}
+
+/*--- Tables ---*/
+table {
+ clear: both;
+ width: 100%;
+ border-collapse: collapse;
+ border-spacing: 0;
+ border: 1px solid #e6e6e6;
+ background: #eaeafa;
+ margin: 12px 0;
+}
+td, th {
+ padding: .25em 1em;
+ border: 1px solid #a6a6c6;
+ vertical-align: middle;
+ text-align: left;
+ font-weight: normal;
+ color: #333;
+ font-size: 0.85em;
+}
+
+table tr {
+ background: #fff;
+}
+
+table.tabular tr:nth-child(even) {
+ background: #eaeafa;
+}
+
+tfoot td {
+ background: #d3d3f3;
+ color: #333;
+ text-align: left;
+ padding: 1em 0.5em;
+ font-size: 0.7em;
+}
+
+th {
+ background: #d3d3f3;
+ color: #333;
+ text-align: left;
+ font-weight: bold;
+ padding: .5em .75em;
+}
+/*--- Forms ---*/
+form {
+ display: block;
+ clear: both;
+ background: #eaeafa;
+ padding: 1em 2em 2em 2em;
+ border: 1px solid #a6a6c6;
+}
+fieldset {
+ padding: 2em;
+ margin: 0 0 1em 0;
+ border: 1px solid #a6a6c6;
+ background: #f3f3f3;
+}
+legend {
+ padding: .5em 1em;
+ border: 1px solid #a6a6c6;
+ background: #fff;
+ font-size: 22px;
+}
+label {
+ padding: 0 1em 0 0;
+ color: #454545;
+ font-weight: normal;
+}
+input[type=text],[type=password], textarea, [type=submit] {
+ font-family: Helvetica, Arial, sans-serif;
+ padding: 2px 4px;
+ border: 1px solid #a6a6c6;
+ color: #454545;
+ font-size: 1em;
+ line-height: 1.25em;
+}
+input[type=text], input[type=password], [type=submit], textarea {
+ clear: both;
+ display: block;
+ padding: .25em .5em;
+}
+input[type=text], input[type=password], textarea {
+ width: 97%;
+ max-width: 950px;
+ margin: .5em 0 1em 0;
+ padding:.5em;
+}
+select {
+ clear: both;
+ display: block;
+ margin: .5em 0 1em 0;
+}
+div.checkbox {
+ clear: both;
+ padding: 1em 0;
+}
+.checkbox label {
+ display: inline;
+}
+
+input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
+ border-color: #00a8e6;
+ outline: none;
+}
+
+/*--- Misc ---*/
+hr {
+ border: none;
+ height: 0;
+ border-bottom: 1px solid #e6e6e6;
+ margin:1em 0;
+}
+sup, sub {
+ color: #666;
+ font-size: .65em;
+}
+acronym {
+ font-weight: bold;
+ font-style: italic;
+ color: #333;
+}
+abbr {
+ color: #333;
+}
+
+blockquote {
+ padding: 0.15em .5em;
+ margin: 0.5em 0;
+ font-size: 2em;
+ color: #666;
+ display: block;
+ font-style: italic;
+}
+
+blockquote:before, blockquote:after {
+ display: inline;
+ color: #e5e5e5;
+ font-size: 3em;
+ position: relative;
+ top: 0.25em;
+ left: -0.1em;
+}
+
+blockquote:before {
+ content: '\D \201C';
+}
+
+
+span.pager-page {
+ font-family: Helvetica, Arial, sans-serif;
+ padding: 3px 3px;
+ margin: 3px 3px 3px 3px;
+ color: #454545;
+ line-height: 1.25em;
+}
+
+input.pager-page {
+ display: inline !important;
+ width: 30px;
+ font-family: Helvetica, Arial, sans-serif;
+ padding: 2px 2px;
+ margin: 3px 3px 3px 3px;
+ color: #454545;
+ line-height: 1.25em;
+ clear: none;
+}
+
+a.pager-page {
+ font-family: Helvetica, Arial, sans-serif;
+ padding: 3px 3px;
+ margin: 3px 3px 3px 3px;
+ border: 1px solid #a6a6c6;
+ color: #454545;
+ line-height: 1.25em;
+ background-color: #efefef;
+}
+
+blockquote:after {
+ content: '\201D';
+}
+
+pre.code {
+ padding: 10px;
+ color: white;
+ background-color: #883434;
+ margin-top: 10px;
+}
+
+/*--- Shadows ---*/
+pre.code {
+ -moz-box-shadow: 0 0 3px rgba(0,0,0,.1);
+ -webkit-box-shadow: 0 0 3px rgba(0,0,0,.1);
+ box-shadow: 0 0 3px rgba(0,0,0,.1);
+}
+table, form {
+ margin-top: 0px;
+ margin-bottom: 12px;
+}
+table, form, pre > code, .shadow {
+ -moz-box-shadow: 2px 2px 12px rgba(0,0,0,.15);
+ -webkit-box-shadow: 2px 2px 12px rgba(0,0,0,0.15);
+ box-shadow: 2px 2px 12px rgba(0,0,0,.15);
+}
+img.shadow {
+ border: 1px solid rgba(255,255,255,.15);
+}
+
+input[type=text],input[type=password], textarea {
+ -moz-box-shadow: inset 0 0 5px rgba(0,0,0,.2);
+ -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,.2);
+ box-shadow: inset 0 0 5px rgba(0,0,0,.2);
+}
165 server.js
@@ -0,0 +1,165 @@
+/**
+ * Very lightweight server, use Express to enable a small amount of flex
+ * Basic requirements for password reset
+ *
+ * TODO: Link to couch to actually delete the user!
+ *
+ */
+var express = require('express'),
+ tokenRegistry = require('./lib/token.registry').TokenRegistry,
+ validatorMixin = require('./lib/validator.mixin'),
+ mailer = require('./lib/emailer').Mailer,
+ check = require('validator').check,
+ sanitize = require('validator').sanitize;
+
+/**
+ * Initial configuration of the Express server
+ */
+var app = express.createServer();
+app.use(express.bodyParser());
+app.use(express.methodOverride());
+app.use(express.static(__dirname + '/public')); // Before router to enable dynamic routing
+app.use(app.router);
+app.use(function(req, res){ res.render('404'); });
+
+/**
+ * Dynamic helpers
+ */
+app.dynamicHelpers({
+ request: function(req){
+ return req;
+ }
+});
+
+
+/**
+ * EJS Views
+ */
+app.set('views', __dirname + '/views');
+app.register('.html', require('ejs'));
+app.set('view engine', 'html');
+
+/**
+ * Development configuration, enables a '/list' route that shows all tokens
+ */
+app.configure('development', function() {
+
+ app.use(express.logger());
+ app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
+
+ app.set('baseUrl','localhost:3000');
+ app.set('helpEmail','<EMAIL>@gmail.com');
+
+ // Enable list in dev mode
+ app.get('/list', function(req,res,next) {
+ res.send(JSON.stringify(tokenRegistry.getCurrent()));
+ });
+
+});
+
+/**
+ * Production configuration, no '/list' route that shows all tokens
+ */
+app.configure('production', function() {
+ app.set('baseUrl','localhost:3000');
+ app.set('helpEmail','<EMAIL>@gmail.com');
+ app.use(express.errorHandler({ dumpExceptions: false, showStack: false }));
+});
+
+
+/**
+ * Home page
+ */
+app.get('/', function(req,res,next) {
+ res.render('home');
+});
+
+/**
+ * Post of request
+ */
+app.post('/reset', function(req,res,next) {
+
+ // Mixin params for validator
+ req.mixinParams();
+
+ var errors = [];
+
+ req.onValidationError(function (msg) {
+ errors.push(msg);
+ });
+
+ //Validate user input
+ req.check('username', 'You need to enter a valid name').notEmpty();
+ req.check('email', 'You need to enter a valid email').isEmail();
+
+ req.sanitize('username').xss();
+ req.sanitize('email').xss();
+
+ var responseData;
+
+ if(errors.length == 0) {
+
+ // Ok - now lets give them a token that lasts 1 day
+ var expires = new Date((new Date()).getTime() + 1000*60*60*24);
+ var tokenId = tokenRegistry.createToken(req.body.username, req.body.email, 'pending', expires);
+
+ // SMTP settings for the emailer are in lib/emailer.js
+ mailer.sendMail({
+ tokenId:tokenId,
+ email:req.params.email,
+ username:req.params.username,
+ baseUrl:app.set('baseUrl'),
+ helpEmail:app.set('helpEmail')
+ }, function(err,result) {
+
+ if(!err) {
+ responseData = {message:'Your request has been submitted, if your details are valid you will receive an email with further instructions.'};
+ res.render("reset",{locals:responseData});
+ } else {
+ responseData = {message:'There was a problem sending you an email:<br/><pre class="code">' + err + '</pre>This is probably because we have misconfigured something on the backend, please try again later.'};
+ res.render("reset",{locals:responseData});
+ }
+
+ });
+
+
+
+ } else {
+
+ responseData = {message:'There were errors in the information you entered: <br><pre class="code">' + JSON.stringify(errors) + '</pre>'};
+ res.render("reset",{locals:responseData});
+
+ }
+
+});
+
+
+/**
+ * Confirmation via the email link
+ */
+app.get('/confirm/:tokenId', function(req,res,next) {
+
+ tokenRegistry.getToken(req.params.tokenId,function(err,token) {
+
+ if(!err) {
+
+ /**
+ * TODO: IF YOU GET TO THIS POINT YOU CAN NOW RESET THE ACCOUNT
+ * account details are in token.username / token.email
+ */
+
+ // Clean up
+ tokenRegistry.removeToken(req.params.tokenId);
+
+ };
+
+ res.render("confirm",{locals:{err:err,token:token}});
+ });
+
+});
+
+
+/**
+ * Launch the server
+ */
+app.listen(3000);
47 test/npm-users.js
@@ -0,0 +1,47 @@
+/**
+ * Dependencies
+ */
+var should = require('should'),
+ Connect = require('connect'),
+ tokenRegistry = require('token.registry').TokenRegistry,
+ uuid = require('uuid.core').UUID;
+
+/**
+ * Simple expresso tests
+ */
+module.exports = {
+
+ /**
+ * Token Registry tests
+ */
+ 'I can generate a unique token id': function(){
+ uuid.generate().should.be.a.string;
+ },
+ 'I can perform basic CRUD on a token that is valid': function() {
+ var tokenId = tokenRegistry.createToken('username', 'user@user.com', 'pending', new Date());
+ tokenRegistry.getToken(tokenId, function(err,token) {
+ should.equal(null,err);
+ token.username.should.equal('username');
+ });
+ tokenRegistry.removeToken(tokenId);
+ tokenRegistry.countTokens().should.equal(0);
+ },
+ 'I cant access a token that doesnt exist': function() {
+ tokenRegistry.getToken('invalid token', function(err,token) {
+ err.should.not.be.null;
+ err.type.should.equal('invalid');
+ should.equal(null,token);
+ });
+ },
+ 'I cant access a token that has expired': function() {
+ var tokenId = uuid.generate();
+ // Add a new token that expired a while back
+ var tokenId = tokenRegistry.createToken('username', 'user@user.com', 'pending', new Date(2010,03,11));
+ tokenRegistry.getToken(tokenId, function(err,token) {
+ err.should.not.be.null;
+ err.type.should.equal('expired');
+ should.equal(null,token);
+ });
+ }
+
+};
2  views/404.html
@@ -0,0 +1,2 @@
+<h1>Cannot find <%= request.url %></h1>
+<p>Sorry we failed to locate this page or resource.</p>
8 views/confirm.html
@@ -0,0 +1,8 @@
+<div class='messages'>
+ <% if(err != null) { %>
+ <%= err.message %>
+ <% } else { %>
+ Your account will now been reset, you will receive an email when it is completed. Do not refresh this page as your one time token has now been deleted.
+ <% }%>
+</div>
+<a href="/">Return</a>
9 views/home.html
@@ -0,0 +1,9 @@
+<p>Please use the form below to request that your account be reset:</p>
+<form method="post" action="/reset">
+ <p><label for="username">NPM Username:</label><input type="text" name="username" /></p>
+ <p><label for="email">NPM Email:</label><input type="text" name="email" /></p>
+ <p>
+ <button class="button-submit" type="submit" value="Update">Submit</button>
+ </p>
+</form>
+<p>If you have forgotten your details, please re-type 'npm adduser' it will tell you your current configured settings (assuming this is the account that you would like reset).</p>
16 views/layout.html
@@ -0,0 +1,16 @@
+<html>
+ <head>
+ <title>NPM User Reset</title>
+ <link rel="stylesheet" href="/css/style.css" />
+ </head>
+ <body>
+ <div id="container">
+ <div id="header">
+ <h1>NPM User Reset</h1>
+ </div>
+ <div id="content">
+ <%- body %>
+ </div>
+ </div>
+ </body>
+</html>
5 views/reset.html
@@ -0,0 +1,5 @@
+<div class='messages'>
+ <%- message %>
+</div>
+<a href="/">Return</a>
+<!-- kind of pointless page but here just in case we want more info -->
Please sign in to comment.
Something went wrong with that request. Please try again.