Initial commit

This commit is contained in:
Selim Mustafaev 2020-02-20 21:38:28 +03:00
commit e9c23d31c8
15 changed files with 2678 additions and 0 deletions

6
.babelrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"plugins":[
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
]
}

33
.eslintrc.js Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
"env": {
"browser": false,
"es6": true,
"webextensions": false,
"node": true,
"mocha": true
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 11 // or 2019
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": 0
}
};

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/index.js"
}
]
}

101
data_providers/avtocod.js Normal file
View File

@ -0,0 +1,101 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const Vehicle = require('../models/vehicle');
const PubNub = require('pubnub');
const baseUrl = 'https://avtocod.ru/api/v3';
let deviceToken = crypto.createHash('sha256').update(Date.now().toString()).digest().toString('hex');
function fromBase64(data) {
return Buffer.from(data, 'base64').toString('binary');
}
async function getPage(url) {
let result = await fetch(url);
return await result.text();
}
async function getJson(url) {
let result = await fetch(url);
return await result.json();
}
function decryptReport(report, hash) {
let dataLength = report.length/2;
let encryptedObject = JSON.parse(fromBase64(report.slice(-dataLength).concat(report.slice(0, -dataLength))));
let iv = Buffer.from(encryptedObject.iv, 'base64');
let data = Buffer.from(encryptedObject.value, 'base64');
let key = crypto.createHash('sha256').update(hash).digest();
let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let decrypted = decipher.update(data);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return JSON.parse(decrypted.toString());
}
function waitForReport(pubnubConfig, channel) {
return new Promise((resolve, reject) => {
let report = null;
let retry = 0;
let timeout = setTimeout(() => {
pubnub.unsubscribeAll();
if(report != null) {
resolve(report);
} else {
reject(new Error('Request timed out'));
}
}, 10000);
let pubnub = new PubNub(pubnubConfig);
pubnub.addListener({
status: function(event) {
if(event.error == true && event.operation == 'PNSubscribeOperation' && event.category == 'PNAccessDeniedCategory' && ++retry < 5) {
setTimeout(() => {
event.errorData.payload.channels.forEach(c => pubnub.subscribe({ channels: [c] }));
}, 1000);
}
},
message: function(message) {
if(message.message.event == 'report-ready') {
clearTimeout(timeout);
pubnub.unsubscribeAll();
resolve(message.message.data);
} else if(message.message.event == 'report-updated') {
report = message.message.data;
}
}
});
pubnub.subscribe({ channels: [channel] });
});
}
class AvtocodProvider {
static async getReport(number) {
let url = `${baseUrl}/auto/generate?number=${encodeURIComponent(number)}&device_token=${deviceToken}`;
let resp = await getJson(url);
console.log('URL: ', resp.report_uri);
let html = await getPage(resp.report_uri);
let result = html.match(/<meta name="app-version-hash" content="(.*?)" \/>/);
if(result == null) {
throw Error('Error getting api version hash');
}
let hash = result[1];
result = html.match(/value:\s+(\{"cloud_payments_public_key".*?\})\s/);
if(result == null) {
throw Error('Error getting report data');
}
let mainData = JSON.parse(result[1]);
let report = decryptReport(mainData.report_container.report, hash);
if(report == null) {
report = await waitForReport(mainData.pubnub, mainData.report_container.channel);
}
return Vehicle.fromAvtocod(report);
}
}
module.exports = AvtocodProvider;

18
index.js Normal file
View File

@ -0,0 +1,18 @@
const express = require('express');
const bodyParser = require('body-parser');
const expressMongoDb = require('./middleware/mongo');
const users = require('./routes/user');
const vehicles = require('./routes/vehicles');
const app = express();
const bearerToken = require('express-bearer-token');
const jwt = require('./middleware/jwt');
app.use(bodyParser.json());
app.use(expressMongoDb('mongodb://autocat_user:autocat#321@vps.aliencat.pro:27017/autocatdev'));
app.use(bearerToken());
app.use(jwt({ secret: 'secret', exclude: ['/user/signup', '/user/login'] }));
app.use('/user', users);
app.use('/vehicles', vehicles);
app.listen(3000);

34
middleware/jwt.js Normal file
View File

@ -0,0 +1,34 @@
const jsonwebtoken = require('jsonwebtoken');
module.exports = function (options) {
return function jwt(req, res, next) {
if('exclude' in options && options.exclude.includes(req.path)) {
next();
return;
}
if (req.headers && req.headers.authorization) {
let parts = req.headers.authorization.split(' ');
if (parts.length == 2) {
let scheme = parts[0];
let token = parts[1];
if (/^Bearer$/i.test(scheme)) {
jsonwebtoken.verify(token, options.secret, (error, decoded) => {
if(error) {
res.status(401).send({ success: false, error: error.message });
} else {
req.user = decoded;
next();
}
});
} else {
res.status(401).send({ success: false, error: 'Unsupported authorization header' });
}
} else {
res.status(401).send({ success: false, error: 'Invalid authorization header' });
}
} else {
res.status(401).send({ success: false, error: 'Missing authorization header' });
}
};
};

25
middleware/mongo.js Normal file
View File

@ -0,0 +1,25 @@
let MongoClient = require('mongodb').MongoClient;
module.exports = function (uri) {
if (typeof uri !== 'string') {
throw new TypeError('Expected uri to be a string');
}
let connection = null;
return function expressMongoDb(req, res, next) {
if (!connection) {
connection = MongoClient.connect(uri, { useUnifiedTopology: true });
}
connection
.then(function (client) {
req['db'] = client.db('autocatdev');
next();
})
.catch(function (err) {
connection = undefined;
next(err);
});
};
};

9
models/engine.js Normal file
View File

@ -0,0 +1,9 @@
class Engine {
number
volume
type
power
fuelType
}
module.exports = Engine;

36
models/user.js Normal file
View File

@ -0,0 +1,36 @@
const crypto = require('crypto');
const uuid = require('uuid/v4');
const hash = Symbol();
const sha256 = text => crypto.createHash('sha256').update(text).digest('base64');
class User {
constructor(login = '', password = '') {
this._id = uuid();
this.login = login;
this[hash] = sha256(password);
}
static fromDB(dbUser) {
let user = new User();
user._id = dbUser._id;
user.login = dbUser.login;
user[hash] = dbUser.hash;
return user;
}
toDB() {
let user = new User();
user._id = this._id;
user.login = this.login;
user.hash = this[hash];
return user;
}
checkPassword(password) {
return this[hash] == sha256(password);
}
}
module.exports = User;

49
models/vehicle.js Normal file
View File

@ -0,0 +1,49 @@
class Vehicle {
brand
model
color
year
category
engine
number
vin1
vin2
sts
pts
isRightWheel
isJapanese
photos
addedDate
addedBy
static fromAvtocod(report) {
let tech = report.fields.tech_data;
let e = tech.engine;
let v = new Vehicle();
v.brand = { name: tech.brand.name, logo: tech.brand.logotype.uri };
v.category = report.fields.additional_info.vehicle.category.code;
v.engine = { number: e.number, volume: e.volume, powerHp: e.power.hp, powerKw: e.power.kw, fuelType: e.fuel.type };
v.model = tech.model ? { name: tech.model.name } : null;
v.year = tech.year;
v.number = report.fields.identifiers.vehicle.reg_num;
v.pts = report.fields.identifiers.vehicle.pts;
v.sts = report.fields.identifiers.vehicle.sts;
v.vin1 = report.fields.identifiers.vehicle.vin;
v.photos = report.fields.images.photos.items.map(p => {
return {
brand: p.vehicle.brand.name,
model: p.vehicle.model.name,
date: Date.parse(p.date.issued),
url: p.uri
};
});
v.isRightWheel = tech.wheel.position != 'LEFT';
v.isJapanese = report.is_japanese_vehicle;
v.addedDate = Date.now();
return v;
}
}
module.exports = Vehicle;

2203
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "autocat",
"version": "1.0.0",
"description": "AutoCat app backend",
"main": "index.js",
"scripts": {
"server": "node --async-stack-traces index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Selim Mustafaev",
"license": "MIT",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"express-bearer-token": "^2.4.0",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.5.2",
"node-fetch": "^2.6.0",
"pubnub": "^4.27.3",
"uuid": "^3.4.0"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
"eslint": "^6.8.0"
}
}

80
routes/user.js Normal file
View File

@ -0,0 +1,80 @@
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/user');
const makeError = error => ({ success: false, error });
router.post('/signup', async (req, res) => {
const { login, password } = req.body;
if(login && password) {
try {
let collection = req.db.collection('users');
let users = await collection.find({ login }).toArray();
if(users.length == 0) {
let user = new User(login, password);
await collection.insertOne(user.toDB());
user.token = jwt.sign({ login }, 'secret', { expiresIn: '1d' });
res.send({ success: true, data: { user } });
} else {
res.send(makeError('User already exists'));
}
} catch(ex) {
res.send(makeError('Error creating user'));
console.error(ex);
}
} else {
res.send(makeError('Invalid parameters'));
}
});
router.post('/login', async (req, res) => {
const { login, password } = req.body;
if(login && password) {
try {
let users = req.db.collection('users');
let me = await users.findOne({ login });
if(me) {
me = User.fromDB(me);
if(!me.checkPassword(password)) {
res.send(makeError('Incorrect login or password'));
return;
}
me.token = jwt.sign({ login }, 'secret', { expiresIn: '1d' });
res.send({ success: true, data: { user: me } });
} else {
res.send(makeError('Incorrect login or password'));
}
} catch(ex) {
res.send(makeError('Error logging in'));
console.error(ex);
}
} else {
res.send(makeError('Invalid parameters'));
}
});
router.get('/', async (req, res) => {
const { login } = req.query;
let users = await req.db.collection('users').find({ login }).toArray();
if(users.length > 0) {
res.send({ success: true, data: User.fromDB(users[0]) });
} else {
res.status(204).send(makeError('There is no such user'));
}
});
router.get('/find', async (req, res) => {
const { login } = req.query;
let users = await req.db.collection('users').find({ login: { $regex: new RegExp(`.*${login}.*`, 'i') } }).toArray();
users = users.map(user => {
user.contacts = [];
return User.fromDB(user);
});
let code = users.length > 0 ? 200 : 204;
res.status(code).send({ success: true, data: { users } });
});
module.exports = router;

40
routes/vehicles.js Normal file
View File

@ -0,0 +1,40 @@
const express = require('express');
const router = express.Router();
const AvtocodProvider = require('../data_providers/avtocod');
const makeError = error => ({ success: false, error });
router.post('/check', async (req, res) => {
const { number } = req.body;
const { login } = req.user;
let collection = req.db.collection('vehicles');
let vehicles = await collection.find({ number }).toArray();
if(vehicles.length > 0) {
res.send({ success: true, data: vehicles[0] });
} else {
try {
let vehicle = await AvtocodProvider.getReport(number);
vehicle.addedBy = login;
await collection.insertOne(vehicle);
res.status(201).send({ success: true, data: vehicle });
} catch(ex) {
res.send(makeError('Error getting report'));
console.error(ex);
}
}
});
router.get('/', async (req, res) => {
const { limit } = req.query;
try {
let collection = req.db.collection('vehicles');
let vehicles = await collection.find().sort({ addedDate: -1 }).limit(parseInt(limit)).toArray();
res.send({ success: true, data: vehicles });
} catch(ex) {
res.send(makeError('Error reading vehicles from DB'));
console.error(ex);
}
});
module.exports = router;