Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ deploy.sh
package-lock.json
launch.json
firebase.json
pnpm-lock.yaml
16 changes: 9 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ const express = require('express'),
https = require('https'),
httpsServer = ((!srv_config.DEBUG && srv_config.CHAIN_PATH &&
srv_config.PRIVATE_KEY_PATH && srv_config.CERTIFICATE_PATH) ? https.createServer({
ca: fs.readFileSync(srv_config.CHAIN_PATH, 'utf-8'),
key: fs.readFileSync(srv_config.PRIVATE_KEY_PATH, 'utf-8'),
cert: fs.readFileSync(srv_config.CERTIFICATE_PATH, 'utf-8')
}, app) : false),
ca: fs.readFileSync(srv_config.CHAIN_PATH, 'utf-8'),
key: fs.readFileSync(srv_config.PRIVATE_KEY_PATH, 'utf-8'),
cert: fs.readFileSync(srv_config.CERTIFICATE_PATH, 'utf-8')
}, app) : false),
Rollbar = require('rollbar'),
rollbar = ((srv_config.ROLLBAR_TOKEN) ? new Rollbar({
accessToken: srv_config.ROLLBAR_TOKEN,
Expand All @@ -32,7 +32,7 @@ const express = require('express'),
getApiVersion: () => '2',
skip: req => {
const path = req.path.toLowerCase();

return path === '/location' || path === '/debug' || path === '/soc' || path === '/extended'
}
}) : false),
Expand Down Expand Up @@ -105,7 +105,7 @@ app.use((req, res, next) => {

// set default headers
app.use((req, res, next) => {
res.contentType('application/json');
//res.contentType('application/json');
res.setHeader('Access-Control-Allow-Origin', ((!req.get('origin') || req.get('origin') === 'null') ? '*' : req.get('origin')));
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
Expand All @@ -118,6 +118,8 @@ app.post('/login', account.login);
app.post('/changepw', account.changePW);
app.get('/settings', settings.getSettings);
app.put('/settings', settings.setSettings);
app.use('/verify/:id', express.static('static/verify'))
app.post('/verify/:id', settings.verifyMail);
app.post('/soc', sync.postSoC);
app.get('/soc', sync.getSoC);
app.post('/extended', sync.postExtended);
Expand Down Expand Up @@ -148,7 +150,7 @@ app.post('/debug', (req, res) => {
req.body.data, req.body.akey, ((parseInt(req.body.timestamp)) ? req.body.timestamp : parseInt(new Date() / 1000))
], (err, dbRes) => {
if (!err && dbRes) {
res.json({status: true});
res.json({ status: true });
} else {
res.status(422).json({
error: srv_errors.UNPROCESSABLE_ENTITY,
Expand Down
25 changes: 24 additions & 1 deletion modules/db/db_template.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
CREATE TABLE IF NOT EXISTS `system` (
`key` VARCHAR(20) NOT NULL PRIMARY KEY,
`value` VARCHAR(20) NOT NULL
);

INSERT INTO `system` VALUES("version", 1) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`);

-- accounts table structure
CREATE TABLE IF NOT EXISTS `accounts` (
`akey` VARCHAR(6) NOT NULL PRIMARY KEY,
Expand All @@ -11,7 +18,6 @@ CREATE TABLE IF NOT EXISTS `settings` (
`user` VARCHAR(6) NOT NULL,
`akey` VARCHAR(6) NOT NULL,
`webhook` VARCHAR(100) DEFAULT NULL,
`email` VARCHAR(1000) DEFAULT NULL,
`telegram` INT(100) DEFAULT 0,
`abrp` VARCHAR(36) DEFAULT NULL,
`summary` TINYINT(1) DEFAULT 0,
Expand All @@ -25,6 +31,23 @@ CREATE TABLE IF NOT EXISTS `settings` (
FOREIGN KEY (`akey`) REFERENCES `accounts`(`akey`)
);

CREATE TABLE IF NOT EXISTS `notificationMail` (
`akey` VARCHAR(6) NOT NULL,
`mail` VARCHAR(1000) NOT NULL,
`verified` BOOLEAN NOT NULL DEFAULT FALSE,
`identifier` BINARY(16) NOT NULL,
PRIMARY KEY (`akey`),
UNIQUE KEY (`identifier`),
FOREIGN KEY (`akey`) REFERENCES `accounts`(`akey`)
);

CREATE TABLE IF NOT EXISTS `mailLock` (
`hash` BINARY(32) NOT NULL,
`time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`weight` INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (`hash`)
);

-- sync table structure
CREATE TABLE IF NOT EXISTS `sync` (
`user` VARCHAR(6) NOT NULL,
Expand Down
10 changes: 8 additions & 2 deletions modules/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ module.exports = {
if (typeof params === 'function') callback = params;
if (!Array.isArray(params)) params = [];
if (typeof sql === 'string') {
if (params.length>0 && Array.isArray(params[0])) {
if (params.length > 0 && Array.isArray(params[0])) {
return db.query(sql, params, callback);
}else {
} else {
return db.query(mysql.format(sql, params), callback);
}
} else if (typeof callback === 'function') callback(srv_errors.INVALID_PARAMETERS);
},
/**
* Gets a database connection from the pool. Requested connections have to be released manually!
*/
getConnection: (callback) => {
db.getConnection(callback);
},
close: callback => db.end(callback),
};
25 changes: 25 additions & 0 deletions modules/db/upgrades/1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS `system` (
`key` VARCHAR(20) NOT NULL PRIMARY KEY,
`value` VARCHAR(20) NOT NULL
);

CREATE TABLE IF NOT EXISTS `notificationMail` (
`akey` VARCHAR(6) NOT NULL PRIMARY KEY,
`mail` VARCHAR(1000) NOT NULL,
`verified` BOOLEAN NOT NULL DEFAULT FALSE,
`identifier` BINARY(16) NOT NULL,
UNIQUE KEY (`identifier`),
FOREIGN KEY (`akey`) REFERENCES `accounts`(`akey`)
);

CREATE TABLE IF NOT EXISTS `mailLock` (
`hash` BINARY(32) NOT NULL PRIMARY KEY,
`time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`weight` INTEGER NOT NULL DEFAULT 1
);

INSERT INTO `notificationMail` SELECT `akey`,`email`,TRUE,UNHEX(MD5(RAND())) FROM `settings` WHERE email IS NOT NULL;

ALTER TABLE `settings`DROP COLUMN `email`;

INSERT INTO `system` VALUES("version", 1);
43 changes: 43 additions & 0 deletions modules/db/upgrades/upgradeSql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const db = require('..');
const path = require('path');
const fs = require('fs')

fs.readdir(".", function (err, files) {
if (err) {
return console.err('Unable to scan directory: ' + err);
}
var max = files.reduce((prev, file) => {
var part = file.split(".")[0];
if (!isNaN(part)) {
return Math.max(prev, parseInt(part));
}
return prev;
}, 0);
if (!max) return console.error('did not find any upgrade files in current directory');

db.getConnection((err, connection) => {
if (err) {
connection.release();
console.error('unable to get connection', err);
return;
}
connection.doQuery = require('util').promisify(connection.query);
connection.doBeginTransaction = require('util').promisify(connection.beginTransaction);
connection.doBeginTransaction()
.then(() => connection.doQuery('CREATE TABLE IF NOT EXISTS `system` (`key` VARCHAR(20) NOT NULL PRIMARY KEY, `value` VARCHAR(20) NOT NULL)'))
.then(() => connection.doQuery('SELECT value FROM system WHERE "key"="version"'))
.then(res => res.length == 0 ? 0 : res.value)
.then(currentVersion => [...Array(max - currentVersion)].reduce((p, _, i) => p.then(() => {
const content = fs.readFileSync((1 + i + currentVersion) + ".sql", "utf-8");
var queries = content.replace(/[\r\n]+/g, ' ').split(";");
return queries.reduce((p, q) => p.then(() => connection.doQuery(q)), Promise.resolve());
}), Promise.resolve()))
.then(connection.commit())
.catch(err => {
connection.rollback();
console.error(err);
})
.then(() => connection.release())
.then(() => db.close());
});
});
4 changes: 2 additions & 2 deletions modules/notification/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ const send = (req, res) => {
});
}
// retrieve required information
db.query('SELECT accounts.akey, token, car, email, telegram, push, lng, soc_display, soc_bms, consumption, last_notification FROM accounts \
INNER JOIN sync ON accounts.akey=sync.akey INNER JOIN settings ON settings.akey=accounts.akey WHERE accounts.akey=?', [
db.query('SELECT accounts.akey, token, car, mail as email, telegram, push, lng, soc_display, soc_bms, consumption, last_notification FROM accounts \
INNER JOIN sync ON accounts.akey=sync.akey INNER JOIN settings ON settings.akey=accounts.akey LEFT JOIN notificationMail ON notificationMail.akey=accounts.akey AND verified=TRUE WHERE accounts.akey=?', [
req.body.akey
], (err, dbRes) => {
if (!err && dbRes && (userObj = dbRes[0]) != null) {
Expand Down
91 changes: 86 additions & 5 deletions modules/notification/mail/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
* @description Mail notification module
*/
const nodemailer = require('nodemailer'),
db = require('./../../db'),
srv_config = require('./../../../srv_config.json'),
srv_errors = require('./../../../srv_errors.json'),
encryption = require('./../../encryption'),
helper = require('./../../helper'),
translation = require('./../../translation');

const transporter = ((!srv_config.MAIL_SERVICE || !srv_config.MAIL_HOST || !srv_config.MAIL_PORT || !srv_config.MAIL_USER ||
!srv_config.MAIL_PASSWORD || !srv_config.MAIL_ADDRESS) ? null :
const doQuery = require('util').promisify(db.query);
const getRandomBytes = require('util').promisify(require('crypto').randomBytes);

const transporter = (((!srv_config.MAIL_SERVICE && (!srv_config.MAIL_HOST || !srv_config.MAIL_PORT)) || !srv_config.MAIL_USER ||
!srv_config.MAIL_PASSWORD || !srv_config.MAIL_ADDRESS) ? null :
nodemailer.createTransport({
host: srv_config.MAIL_HOST,
port: srv_config.MAIL_PORT,
Expand Down Expand Up @@ -46,14 +50,14 @@ const sendMail = (userObj, abort) => {
SOC: ((
userObj.soc_display == null) ? SOC_BMS : ((
userObj.soc_bms == null) ?
SOC_DISPLAY : SOC_DISPLAY))
SOC_DISPLAY : SOC_DISPLAY))

}, // use only defined values for text
textObj = {
SOC: ((
userObj.soc_display == null) ? '<b>' + SOC_BMS + '</b> (BMS)' : ((
userObj.soc_bms == null) ?
'<b>' + SOC_DISPLAY + '</b> (Display)' : '<b>' + SOC_DISPLAY + '</b> (Display) / ' + SOC_BMS + ' (BMS)')),
'<b>' + SOC_DISPLAY + '</b> (Display)' : '<b>' + SOC_DISPLAY + '</b> (Display) / ' + SOC_BMS + ' (BMS)')),
RANGE: helper.calculateRange(userObj.car, userObj.soc_display || userObj.soc_bms, userObj.consumption) + 'km'
};

Expand Down Expand Up @@ -108,9 +112,86 @@ const simpleSend = (mail, subject, html, attachments, callback) => {
});
};

const setMail = (userObj, mail, callback) => {
const akey = userObj.akey;
if (!mail) {
return doQuery('DELETE FROM notificationMail WHERE akey=?', [akey]).then(() => callback(false)).catch(callback);
}
if (!validateMail(mail)) {
if (typeof callback === 'function') callback(srv_errors.INVALID_PARAMETERS);
return;
}
doQuery('SELECT mail FROM notificationMail WHERE akey=? AND verified=TRUE', [akey])
.then(result => {
if (result.length > 0 && encryption.decrypt(result[0].mail) === mail) return Promise.reject(srv_errors.CURRENT_MAIL);
return true;
})
.then(() => module.exports.checkMailUnlocked(mail))
.then(() => getRandomBytes(16))
.then(id => new Promise((res, rej) => {
module.exports.simpleSend(mail, translation.translate('MAIL_SUBJECT_VERIFY', userObj.lng, true),
translation.translateWithData('MAIL_TEXT_VERIFY', userObj.lng, { BASE_URL: srv_config.BASE_URL, ID: id.toString('hex') }, true), null, (err) => {
if (err) {
if (err.responseCode === 550) return rej(srv_errors.INVALID_MAIL)
return rej(err);
}
return res(id);
});
}))
.then(id => doQuery('INSERT INTO notificationMail(akey,mail,verified,identifier) VALUES(?,?,false,?) ON DUPLICATE KEY UPDATE mail=VALUES(mail), verified=false, identifier=VALUES(identifier)', [akey, encryption.encrypt(mail), id]))
.then(() => module.exports.updateMailLock(mail))
.then(() => callback(false)).catch(callback);
};

const verifyMail = (identifier, callback) => {
doQuery('UPDATE notificationMail SET verified=TRUE WHERE identifier=UNHEX(?)', [identifier]).then(queryResult => {
if (queryResult.affectedRows !== 1) return Promise.reject(srv_errors.NOT_FOUND);
if (queryResult.changedRows !== 1) return Promise.reject(srv_errors.CONFLICT);
return true;
}).then(() => callback(false)).catch(callback);
}

const checkMailUnlocked = mail => {
return doQuery('SELECT time FROM mailLock WHERE hash=UNHEX(SHA2(?,256))', [mail]).then(dbRes => {
if (dbRes.length === 0) return mail;
if (dbRes[0].time.getTime() <= new Date().getTime()) return mail;
throw new Error('Recipient is currently locked')
});
}

const updateQuery = `INSERT INTO
mailLock
SELECT
hash,
TIMESTAMPADD(MINUTE, POWER(weight + 1, 2), CURRENT_TIMESTAMP()) as time,
weight + 1 as weight
FROM
mailLock
WHERE
hash = UNHEX(SHA2(?, 256))
AND time > TIMESTAMPADD(MINUTE, - POWER(weight + 1, 3), CURRENT_TIMESTAMP())
UNION
SELECT
UNHEX(SHA2(?, 256)),
CURRENT_TIMESTAMP() as time,
1 as weight
LIMIT
1
ON DUPLICATE KEY UPDATE
time = VALUES(time),
weight = values(weight)
`
const updateMailLock = mail => {
return doQuery(updateQuery, [mail, mail]);
}

module.exports = {
validateMail,
sendMail,
sendQRMail,
simpleSend
simpleSend,
checkMailUnlocked,
updateMailLock,
setMail,
verifyMail,
};
Loading