Initial commit
This commit is contained in:
commit
e9c23d31c8
6
.babelrc.json
Normal file
6
.babelrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"plugins":[
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-proposal-private-methods"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
.eslintrc.js
Normal file
33
.eslintrc.js
Normal 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
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal 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
101
data_providers/avtocod.js
Normal 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
18
index.js
Normal 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
34
middleware/jwt.js
Normal 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
25
middleware/mongo.js
Normal 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
9
models/engine.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class Engine {
|
||||||
|
number
|
||||||
|
volume
|
||||||
|
type
|
||||||
|
power
|
||||||
|
fuelType
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Engine;
|
||||||
36
models/user.js
Normal file
36
models/user.js
Normal 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
49
models/vehicle.js
Normal 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
2203
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal 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
80
routes/user.js
Normal 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
40
routes/vehicles.js
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user