Move backend files to backend/ subdirectory

This commit is contained in:
2025-07-25 19:09:35 -03:00
parent d04304b573
commit 1bd4ca0d98
59 changed files with 0 additions and 0 deletions

View 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;

View 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
View 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
View 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
View 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
View 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
};