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