Move backend files to backend/ subdirectory
This commit is contained in:
92
backend/modules/authentication.js
Normal file
92
backend/modules/authentication.js
Normal file
@@ -0,0 +1,92 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
// larger numbers mean better security, less
|
||||
var config = {
|
||||
digest: 'sha512',
|
||||
// size of the generated hash
|
||||
hashBytes: 32,
|
||||
// larger salt means hashed passwords are more resistant to rainbow table, but
|
||||
// you get diminishing returns pretty fast
|
||||
saltBytes: 24,
|
||||
// more iterations means an attacker has to take longer to brute force an
|
||||
// individual password, so larger is better. however, larger also means longer
|
||||
// to hash the password. tune so that hashing the password takes about a
|
||||
// second
|
||||
iterations: 233335
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash a password using Node's asynchronous pbkdf2 (key derivation) function.
|
||||
*
|
||||
* Returns a self-contained buffer which can be arbitrarily encoded for storage
|
||||
* that contains all the data needed to verify a password.
|
||||
*
|
||||
* @param {!String} password
|
||||
* @param {!function(?Error, ?Buffer=)} callback
|
||||
*/
|
||||
function hashPassword (password, callback) {
|
||||
// generate a salt for pbkdf2
|
||||
crypto.randomBytes(config.saltBytes, function (err, salt) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
crypto.pbkdf2(password, salt, config.iterations, config.hashBytes, config.digest,
|
||||
function (err, hash) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var combined = Buffer.alloc((hash.length + salt.length + 8));
|
||||
|
||||
// include the size of the salt so that we can, during verification,
|
||||
// figure out how much of the hash is salt
|
||||
combined.writeUInt32BE(salt.length, 0, true);
|
||||
// similarly, include the iteration count
|
||||
combined.writeUInt32BE(config.iterations, 4, true);
|
||||
|
||||
salt.copy(combined, 8);
|
||||
hash.copy(combined, salt.length + 8);
|
||||
callback(null, combined);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a password using Node's asynchronous pbkdf2 (key derivation) function.
|
||||
*
|
||||
* Accepts a hash and salt generated by hashPassword, and returns whether the
|
||||
* hash matched the password (as a boolean).
|
||||
*
|
||||
* @param {!String} password
|
||||
* @param {!Buffer} combined Buffer containing hash and salt as generated by
|
||||
* hashPassword.
|
||||
* @param {!function(?Error, !boolean)}
|
||||
*/
|
||||
function verifyPassword (password, combined, callback) {
|
||||
// extract the salt and hash from the combined buffer
|
||||
var saltBytes = combined.readUInt32BE(0);
|
||||
var hashBytes = combined.length - saltBytes - 8;
|
||||
var iterations = combined.readUInt32BE(4);
|
||||
var salt = combined.slice(8, saltBytes + 8);
|
||||
var hash = combined.toString('binary', saltBytes + 8);
|
||||
|
||||
// verify the salt and hash against the password
|
||||
crypto.pbkdf2(password, salt, iterations, hashBytes, config.digest, function(err, verify) {
|
||||
if (err && typeof callback === 'function') {
|
||||
return callback(err, false);
|
||||
} else if (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback(null, verify.toString('binary') === hash);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exports.hashPassword = hashPassword;
|
||||
exports.verifyPassword = verifyPassword;
|
||||
60
backend/modules/geocoder.js
Normal file
60
backend/modules/geocoder.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const NodeGeocoder = require('node-geocoder');
|
||||
|
||||
function addressToString (address) {
|
||||
var string = '';
|
||||
string = address.address1 ? address.street1 + ',' : '';
|
||||
string = string + (address.locality ? ' ' + address.locality + ',' : '');
|
||||
string = string + (address.region ? ' ' + address.region : '');
|
||||
string = string + (address.postal ? ' ' + address.postal : '');
|
||||
string = string + (address.country ? ' ' + address.country : '');
|
||||
return string;
|
||||
}
|
||||
|
||||
var options = {
|
||||
provider: 'google',
|
||||
// Optional depending on the providers
|
||||
httpAdapter: 'https', // Default
|
||||
apiKey: 'AIzaSyCvpBGztvxtRUNigOW9f0GXVRWlukJZsps', // for Mapquest, OpenCage, Google Premier
|
||||
formatter: null // 'gpx', 'string', ...
|
||||
};
|
||||
|
||||
var geocoder = NodeGeocoder(options);
|
||||
|
||||
|
||||
|
||||
exports.addressToString = addressToString;
|
||||
exports.geocoder = geocoder;
|
||||
|
||||
|
||||
// Using callback
|
||||
// geocoder.geocode('29 champs elysée paris', function(err, res) {
|
||||
// console.log(res);
|
||||
// });
|
||||
//
|
||||
// // Or using Promise
|
||||
// geocoder.geocode('29 champs elysée paris')
|
||||
// .then(function(res) {
|
||||
// console.log(res);
|
||||
// })
|
||||
// .catch(function(err) {
|
||||
// console.log(err);
|
||||
// });
|
||||
|
||||
// output :
|
||||
// [{
|
||||
// latitude: 48.8698679,
|
||||
// longitude: 2.3072976,
|
||||
// country: 'France',
|
||||
// countryCode: 'FR',
|
||||
// city: 'Paris',
|
||||
// zipcode: '75008',
|
||||
// streetName: 'Champs-Élysées',
|
||||
// streetNumber: '29',
|
||||
// administrativeLevels: {
|
||||
// level1long: 'Île-de-France',
|
||||
// level1short: 'IDF',
|
||||
// level2long: 'Paris',
|
||||
// level2short: '75'
|
||||
// },
|
||||
// provider: 'google'
|
||||
// }]
|
||||
63
backend/modules/images.js
Normal file
63
backend/modules/images.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const fs = require('fs');
|
||||
const Logger = require('./logger');
|
||||
const ShortId = require('shortid');
|
||||
|
||||
const ATTACHMENT_STORE = '../images';
|
||||
const ATTACHMENT_STORE_PROFILE = '/profile';
|
||||
const ATTACHMENT_STORE_MESSAGE = '/message';
|
||||
const ATTACHMENT_SUFFIX_DETAIL = '_detail';
|
||||
const ATTACHMENT_SUFFIX_THUMBNAIL = '_thumbnail';
|
||||
|
||||
function generateFilename (filename, type = 'detail') {
|
||||
var re = /(?:\.([^.]+))?$/;
|
||||
var ext = re.exec(filename)[1];
|
||||
return '' + ShortId.generate() + (type === 'thumbnail' ? ATTACHMENT_SUFFIX_THUMBNAIL : ATTACHMENT_SUFFIX_DETAIL) + "." + ext;
|
||||
}
|
||||
|
||||
function processImage (data, context, type, callback) {
|
||||
var folder = ATTACHMENT_STORE + (context === 'profile' ? ATTACHMENT_STORE_PROFILE : ATTACHMENT_STORE_MESSAGE);
|
||||
var filename = generateFilename(data.imageFilename, type);
|
||||
|
||||
var dataUrl = data.image;
|
||||
var matches = dataUrl.match(/^data:.+\/(.+);base64,(.*)$/);
|
||||
var base64Data = matches[2];
|
||||
var buffer = new Buffer(base64Data, 'base64');
|
||||
|
||||
saveImage(folder + '/' + filename, buffer, callback);
|
||||
}
|
||||
|
||||
function saveImage (filename, data, callback = noop) {
|
||||
fs.writeFile(filename, data, function (err, stat) {
|
||||
if (err) {
|
||||
Logger.error('[Images.saveImage] Image save failure.', { err: err, stat: stat, filename: filename });
|
||||
callback(err, null);
|
||||
} else {
|
||||
Logger.debug('[Images.saveImage] Image save successful.', { stat: stat, filename: filename });
|
||||
callback(null, filename.substring(2));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function noop (err, result) {
|
||||
Logger.debug('No callback function supplied.', { err: err, result: result });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
save: (data, filename, callback = noop) => {
|
||||
saveImage(filename, data, callback);
|
||||
},
|
||||
|
||||
saveMessageImage: (data, callback) => {
|
||||
processImage(data, 'message', 'detail', callback);
|
||||
},
|
||||
|
||||
saveProfileDetailImage: (data, callback) => {
|
||||
processImage(data, 'profile', 'detail', callback);
|
||||
},
|
||||
|
||||
saveProfileThumbnailImage: (data, callback) => {
|
||||
processImage(data, 'profile', 'thumbnail', callback);
|
||||
}
|
||||
};
|
||||
|
||||
22
backend/modules/logger.js
Normal file
22
backend/modules/logger.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const winston = require('winston');
|
||||
|
||||
var logger = new (winston.Logger)({
|
||||
transports: [
|
||||
new (winston.transports.File)({
|
||||
name: 'debug-file',
|
||||
filename: 'urge-debug.log',
|
||||
handleExceptions: true,
|
||||
humanReadableUnhandledException: true,
|
||||
level: 'debug'
|
||||
}),
|
||||
new (winston.transports.File)({
|
||||
name: 'error-file',
|
||||
filename: 'urge-error.log',
|
||||
handleExceptions: true,
|
||||
humanReadableUnhandledException: true,
|
||||
level: 'error'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
32
backend/modules/mailer.js
Normal file
32
backend/modules/mailer.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const Mailer = require('nodemailer');
|
||||
|
||||
function sendMail (options, callback) {
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
let transporter = Mailer.createTransport({
|
||||
host: 'mail.fitz.guru',
|
||||
port: 587,
|
||||
secure: false, // secure:true for port 465, secure:false for port 587
|
||||
auth: {
|
||||
user: 'support@fitz.guru',
|
||||
pass: 'NotSt@ff3d!'
|
||||
}
|
||||
});
|
||||
|
||||
callback = typeof callback === 'function' ? callback : (error, info) => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
console.debug('Message %s sent: %s', info.messageId, info.response);
|
||||
};
|
||||
|
||||
// send mail with defined transport object
|
||||
transporter.sendMail(options, callback);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
send: (email, callback) => {
|
||||
sendMail(mail, callback);
|
||||
}
|
||||
};
|
||||
110
backend/modules/token.js
Normal file
110
backend/modules/token.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const JWT = require('jsonwebtoken');
|
||||
const KEY = 'Th1s is THE s3cr3t kEy. It secures the t0ken!';
|
||||
|
||||
const Token = {
|
||||
create: (payload, expires = '1h', callback) => {
|
||||
JWT.sign(payload, KEY, { expiresIn: expires }, callback);
|
||||
},
|
||||
verify: (token, callback) => {
|
||||
JWT.verify(token, KEY, callback);
|
||||
}
|
||||
};
|
||||
|
||||
function createAnonymousToken (e) {
|
||||
Token.create({ username: null, can: ['view'] }, (err, token) => {
|
||||
if (err) {
|
||||
e.emit('token:create', err, null);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
e.emit('token:create', null, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createAuthenticatedToken (e, user, expires = '1h', event = 'token:create') {
|
||||
Token.create({ username: user.name, can: user.can }, expires, (err, token) => {
|
||||
if (err) {
|
||||
e.emit(event, err, null);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
e.emit(event, null, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshToken (e, token) {
|
||||
Token.verify(e, token, (err, decoded) => {
|
||||
if (err) {
|
||||
e.emit('token:refresh', err, null);
|
||||
}
|
||||
|
||||
if (decoded) {
|
||||
createAuthenticatedToken(
|
||||
e,
|
||||
{ username: decoded.username, can: decoded.can },
|
||||
'token:refresh'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateToken (e, token, callback) {
|
||||
if (token) {
|
||||
token = token.replace(/(bearer|basic)\s/i, '');
|
||||
|
||||
Token.verify(token, (err, decoded) => {
|
||||
var result = { valid: !!decoded, data: decoded };
|
||||
|
||||
if (e) {
|
||||
if (err || !result.valid) {
|
||||
e.emit('token:validate', (err || result), null);
|
||||
}
|
||||
|
||||
e.emit('token:validate', null, result);
|
||||
}
|
||||
|
||||
else if (typeof callback === 'function') {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (e) {
|
||||
e.emit('token:validate', 'No session token passed.', null);
|
||||
}
|
||||
|
||||
else if (typeof callback === 'function') {
|
||||
callback('No session token passed.', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyTokenThen (token, action, callback) {
|
||||
if (action === 'view') {
|
||||
callback(null, { hasPermission: true });
|
||||
} else {
|
||||
validateToken(null, token, (err, decoded) => {
|
||||
if (err) {
|
||||
callback('Session could not be validated.', null);
|
||||
}
|
||||
|
||||
if (decoded) {
|
||||
callback(null, { hasPermission: (decoded.valid && (decoded.can.indexOf(action) > -1)) });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
anonymous: createAnonymousToken,
|
||||
create: createAuthenticatedToken,
|
||||
refresh: refreshToken,
|
||||
validate: validateToken,
|
||||
verifyThen: verifyTokenThen
|
||||
};
|
||||
Reference in New Issue
Block a user