chore: remove old-api
This commit is contained in:
parent
4b270877ca
commit
004deb8e60
|
@ -1,31 +0,0 @@
|
||||||
FROM node:15.14-buster
|
|
||||||
|
|
||||||
# Install python3, pip3, and make them the default for `python` and `pip` commands
|
|
||||||
RUN apt-get update && apt-get install -y python3 python3-pip
|
|
||||||
RUN ln -s $(which python3) /usr/local/bin/python
|
|
||||||
RUN ln -s $(which pip3) /usr/local/bin/pip
|
|
||||||
|
|
||||||
WORKDIR /opt/obs/api
|
|
||||||
|
|
||||||
ADD package.json package-lock.json /opt/obs/api/
|
|
||||||
RUN echo update-notifier=false >> ~/.npmrc
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
ADD scripts /opt/obs/api/scripts/
|
|
||||||
ADD tools /opt/obs/api/tools/
|
|
||||||
ADD requirements.txt /opt/obs/api/
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
RUN pip install -e ./scripts
|
|
||||||
|
|
||||||
ADD views /opt/obs/api/views/
|
|
||||||
ADD src /opt/obs/api/src/
|
|
||||||
|
|
||||||
#ADD .migrations.js .
|
|
||||||
#ADD migrations .
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV PORT=3000
|
|
||||||
ENV DATA_DIR=/data
|
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
|
165
old-api/LICENSE
165
old-api/LICENSE
|
@ -1,165 +0,0 @@
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
|
|
||||||
This version of the GNU Lesser General Public License incorporates
|
|
||||||
the terms and conditions of version 3 of the GNU General Public
|
|
||||||
License, supplemented by the additional permissions listed below.
|
|
||||||
|
|
||||||
0. Additional Definitions.
|
|
||||||
|
|
||||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
||||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
||||||
General Public License.
|
|
||||||
|
|
||||||
"The Library" refers to a covered work governed by this License,
|
|
||||||
other than an Application or a Combined Work as defined below.
|
|
||||||
|
|
||||||
An "Application" is any work that makes use of an interface provided
|
|
||||||
by the Library, but which is not otherwise based on the Library.
|
|
||||||
Defining a subclass of a class defined by the Library is deemed a mode
|
|
||||||
of using an interface provided by the Library.
|
|
||||||
|
|
||||||
A "Combined Work" is a work produced by combining or linking an
|
|
||||||
Application with the Library. The particular version of the Library
|
|
||||||
with which the Combined Work was made is also called the "Linked
|
|
||||||
Version".
|
|
||||||
|
|
||||||
The "Minimal Corresponding Source" for a Combined Work means the
|
|
||||||
Corresponding Source for the Combined Work, excluding any source code
|
|
||||||
for portions of the Combined Work that, considered in isolation, are
|
|
||||||
based on the Application, and not on the Linked Version.
|
|
||||||
|
|
||||||
The "Corresponding Application Code" for a Combined Work means the
|
|
||||||
object code and/or source code for the Application, including any data
|
|
||||||
and utility programs needed for reproducing the Combined Work from the
|
|
||||||
Application, but excluding the System Libraries of the Combined Work.
|
|
||||||
|
|
||||||
1. Exception to Section 3 of the GNU GPL.
|
|
||||||
|
|
||||||
You may convey a covered work under sections 3 and 4 of this License
|
|
||||||
without being bound by section 3 of the GNU GPL.
|
|
||||||
|
|
||||||
2. Conveying Modified Versions.
|
|
||||||
|
|
||||||
If you modify a copy of the Library, and, in your modifications, a
|
|
||||||
facility refers to a function or data to be supplied by an Application
|
|
||||||
that uses the facility (other than as an argument passed when the
|
|
||||||
facility is invoked), then you may convey a copy of the modified
|
|
||||||
version:
|
|
||||||
|
|
||||||
a) under this License, provided that you make a good faith effort to
|
|
||||||
ensure that, in the event an Application does not supply the
|
|
||||||
function or data, the facility still operates, and performs
|
|
||||||
whatever part of its purpose remains meaningful, or
|
|
||||||
|
|
||||||
b) under the GNU GPL, with none of the additional permissions of
|
|
||||||
this License applicable to that copy.
|
|
||||||
|
|
||||||
3. Object Code Incorporating Material from Library Header Files.
|
|
||||||
|
|
||||||
The object code form of an Application may incorporate material from
|
|
||||||
a header file that is part of the Library. You may convey such object
|
|
||||||
code under terms of your choice, provided that, if the incorporated
|
|
||||||
material is not limited to numerical parameters, data structure
|
|
||||||
layouts and accessors, or small macros, inline functions and templates
|
|
||||||
(ten or fewer lines in length), you do both of the following:
|
|
||||||
|
|
||||||
a) Give prominent notice with each copy of the object code that the
|
|
||||||
Library is used in it and that the Library and its use are
|
|
||||||
covered by this License.
|
|
||||||
|
|
||||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
||||||
document.
|
|
||||||
|
|
||||||
4. Combined Works.
|
|
||||||
|
|
||||||
You may convey a Combined Work under terms of your choice that,
|
|
||||||
taken together, effectively do not restrict modification of the
|
|
||||||
portions of the Library contained in the Combined Work and reverse
|
|
||||||
engineering for debugging such modifications, if you also do each of
|
|
||||||
the following:
|
|
||||||
|
|
||||||
a) Give prominent notice with each copy of the Combined Work that
|
|
||||||
the Library is used in it and that the Library and its use are
|
|
||||||
covered by this License.
|
|
||||||
|
|
||||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
||||||
document.
|
|
||||||
|
|
||||||
c) For a Combined Work that displays copyright notices during
|
|
||||||
execution, include the copyright notice for the Library among
|
|
||||||
these notices, as well as a reference directing the user to the
|
|
||||||
copies of the GNU GPL and this license document.
|
|
||||||
|
|
||||||
d) Do one of the following:
|
|
||||||
|
|
||||||
0) Convey the Minimal Corresponding Source under the terms of this
|
|
||||||
License, and the Corresponding Application Code in a form
|
|
||||||
suitable for, and under terms that permit, the user to
|
|
||||||
recombine or relink the Application with a modified version of
|
|
||||||
the Linked Version to produce a modified Combined Work, in the
|
|
||||||
manner specified by section 6 of the GNU GPL for conveying
|
|
||||||
Corresponding Source.
|
|
||||||
|
|
||||||
1) Use a suitable shared library mechanism for linking with the
|
|
||||||
Library. A suitable mechanism is one that (a) uses at run time
|
|
||||||
a copy of the Library already present on the user's computer
|
|
||||||
system, and (b) will operate properly with a modified version
|
|
||||||
of the Library that is interface-compatible with the Linked
|
|
||||||
Version.
|
|
||||||
|
|
||||||
e) Provide Installation Information, but only if you would otherwise
|
|
||||||
be required to provide such information under section 6 of the
|
|
||||||
GNU GPL, and only to the extent that such information is
|
|
||||||
necessary to install and execute a modified version of the
|
|
||||||
Combined Work produced by recombining or relinking the
|
|
||||||
Application with a modified version of the Linked Version. (If
|
|
||||||
you use option 4d0, the Installation Information must accompany
|
|
||||||
the Minimal Corresponding Source and Corresponding Application
|
|
||||||
Code. If you use option 4d1, you must provide the Installation
|
|
||||||
Information in the manner specified by section 6 of the GNU GPL
|
|
||||||
for conveying Corresponding Source.)
|
|
||||||
|
|
||||||
5. Combined Libraries.
|
|
||||||
|
|
||||||
You may place library facilities that are a work based on the
|
|
||||||
Library side by side in a single library together with other library
|
|
||||||
facilities that are not Applications and are not covered by this
|
|
||||||
License, and convey such a combined library under terms of your
|
|
||||||
choice, if you do both of the following:
|
|
||||||
|
|
||||||
a) Accompany the combined library with a copy of the same work based
|
|
||||||
on the Library, uncombined with any other library facilities,
|
|
||||||
conveyed under the terms of this License.
|
|
||||||
|
|
||||||
b) Give prominent notice with the combined library that part of it
|
|
||||||
is a work based on the Library, and explaining where to find the
|
|
||||||
accompanying uncombined form of the same work.
|
|
||||||
|
|
||||||
6. Revised Versions of the GNU Lesser General Public License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the GNU Lesser General Public License from time to time. Such new
|
|
||||||
versions will be similar in spirit to the present version, but may
|
|
||||||
differ in detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Library as you received it specifies that a certain numbered version
|
|
||||||
of the GNU Lesser General Public License "or any later version"
|
|
||||||
applies to it, you have the option of following the terms and
|
|
||||||
conditions either of that published version or of any later version
|
|
||||||
published by the Free Software Foundation. If the Library as you
|
|
||||||
received it does not specify a version number of the GNU Lesser
|
|
||||||
General Public License, you may choose any version of the GNU Lesser
|
|
||||||
General Public License ever published by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Library as you received it specifies that a proxy can decide
|
|
||||||
whether future versions of the GNU Lesser General Public License shall
|
|
||||||
apply, that proxy's public statement of acceptance of any version is
|
|
||||||
permanent authorization for you to choose that version for the
|
|
||||||
Library.
|
|
|
@ -1,61 +0,0 @@
|
||||||
const Track = require('../src/models/Track');
|
|
||||||
const { replaceDollarNewlinesHack, detectFormat, buildObsver1 } = require('../src/logic/tracks');
|
|
||||||
|
|
||||||
function shouldRebuildBody(track) {
|
|
||||||
if (!track.trackData || !track.trackData.points.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!track.body) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const body = track.body.trim();
|
|
||||||
if (!body) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualBody = replaceDollarNewlinesHack(body).trim();
|
|
||||||
if (body !== actualBody) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineCount = (actualBody.match(/\n/g) || []).length + 1;
|
|
||||||
|
|
||||||
const format = detectFormat(body);
|
|
||||||
if (format === 'invalid') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// never reconstruct body of version 2
|
|
||||||
if (format > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not enough data in the file
|
|
||||||
if (lineCount < track.trackData.points.length + 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function up(next) {
|
|
||||||
const query = Track.find().populate('trackData');
|
|
||||||
for await (const track of query) {
|
|
||||||
const rebuild = shouldRebuildBody(track);
|
|
||||||
if (rebuild) {
|
|
||||||
track.body = buildObsver1(track.trackData.points);
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function down(next) {
|
|
||||||
// nothing to do
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { up, down };
|
|
|
@ -1,15 +0,0 @@
|
||||||
const Track = require('../src/models/Track');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(next) {
|
|
||||||
for await (const track of Track.find()) {
|
|
||||||
await track.rebuildTrackDataAndSave();
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(next) {
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
const Track = require('../src/models/Track');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(next) {
|
|
||||||
try {
|
|
||||||
for await (const track of Track.find()) {
|
|
||||||
track.originalFileName = track.slug + '.csv'
|
|
||||||
await track.generateOriginalFilePath();
|
|
||||||
await track.save()
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(next) {
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
const Track = require('../src/models/Track');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
async up(next) {
|
|
||||||
try {
|
|
||||||
for await (const track of Track.find()) {
|
|
||||||
if (!track.body) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.writeToOriginalFile(track.body)
|
|
||||||
delete track.body;
|
|
||||||
await track.save()
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
} catch(err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(next) {
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
};
|
|
9419
old-api/package-lock.json
generated
9419
old-api/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,116 +0,0 @@
|
||||||
{
|
|
||||||
"name": "open-bike-sensor-web-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Backend API for the OpenBikeSensor web app",
|
|
||||||
"main": "app.js",
|
|
||||||
"scripts": {
|
|
||||||
"mongo:start": "docker run --name realworld-mongo -p 27017:27017 mongo & sleep 5",
|
|
||||||
"start": "node src/",
|
|
||||||
"start:worker": "node src/worker.js",
|
|
||||||
"dev": "nodemon src/",
|
|
||||||
"dev:worker": "nodemon -w src/ src/worker.js",
|
|
||||||
"mongo:stop": "docker stop realworld-mongo && docker rm realworld-mongo",
|
|
||||||
"autoformat": "eslint --fix .",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"test": "jest",
|
|
||||||
"migrate": "mongoose-data-migrate -c .migrations.js",
|
|
||||||
"migrate:up": "npm run migrate -- up",
|
|
||||||
"migrate:down": "npm run migrate -- down"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/openbikesensor/obsAPI.git"
|
|
||||||
},
|
|
||||||
"license": "LGPLv3",
|
|
||||||
"dependencies": {
|
|
||||||
"body-parser": "1.19.0",
|
|
||||||
"bull": "^3.22.0",
|
|
||||||
"connect-busboy": "0.0.2",
|
|
||||||
"cors": "2.8.5",
|
|
||||||
"csv-parse": "^4.15.1",
|
|
||||||
"csv-stringify": "^5.6.1",
|
|
||||||
"ejs": "^3.1.6",
|
|
||||||
"errorhandler": "1.5.1",
|
|
||||||
"express": "4.17.1",
|
|
||||||
"express-jwt": "^6.0.0",
|
|
||||||
"express-session": "1.17.1",
|
|
||||||
"jest": "^26.6.3",
|
|
||||||
"joi": "^17.4.0",
|
|
||||||
"jsonwebtoken": "8.5.1",
|
|
||||||
"luxon": "^1.26.0",
|
|
||||||
"method-override": "3.0.0",
|
|
||||||
"methods": "1.1.2",
|
|
||||||
"mongoose": "^6.0.5",
|
|
||||||
"mongoose-data-migrate": "flashstockinc/mongoose-data-migrate",
|
|
||||||
"mongoose-unique-validator": "2.0.3",
|
|
||||||
"morgan": "1.10.0",
|
|
||||||
"nodemailer": "^6.4.18",
|
|
||||||
"oauth2orize": "^1.11.0",
|
|
||||||
"passport": "0.4.1",
|
|
||||||
"passport-custom": "^1.1.1",
|
|
||||||
"passport-http-bearer": "^1.0.1",
|
|
||||||
"passport-jwt": "^4.0.0",
|
|
||||||
"passport-local": "^1.0.0",
|
|
||||||
"pkce": "^1.0.0-beta2",
|
|
||||||
"pug": "^3.0.1",
|
|
||||||
"request": "2.88.2",
|
|
||||||
"sanitize-filename": "^1.6.3",
|
|
||||||
"semantic-ui-css": "^2.4.1",
|
|
||||||
"slug": "^3.5.2",
|
|
||||||
"turf": "^3.0.14",
|
|
||||||
"underscore": "^1.12.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^7.20.0",
|
|
||||||
"eslint-config-prettier": "^6.15.0",
|
|
||||||
"eslint-config-standard": "^16.0.2",
|
|
||||||
"eslint-plugin-import": "^2.22.1",
|
|
||||||
"eslint-plugin-jest": "^24.1.5",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-prettier": "^3.3.1",
|
|
||||||
"eslint-plugin-promise": "^4.3.1",
|
|
||||||
"nodemon": "^2.0.7",
|
|
||||||
"prettier": "^2.2.1"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"modulePathIgnorePatterns": [
|
|
||||||
"local"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"prettier": {
|
|
||||||
"useTabs": false,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 120
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"standard",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"jest",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": false,
|
|
||||||
"node": true,
|
|
||||||
"jest/globals": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"standard/array-bracket-even-spacing": 0,
|
|
||||||
"standard/computed-property-even-spacing": 0,
|
|
||||||
"standard/object-curly-even-spacing": 0
|
|
||||||
},
|
|
||||||
"root": true,
|
|
||||||
"ignorePatterns": [
|
|
||||||
"postman-examples/**",
|
|
||||||
"public/**",
|
|
||||||
"node_modules",
|
|
||||||
"local"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
./scripts
|
|
||||||
sqlalchemy[asyncio]
|
|
||||||
asyncpg
|
|
|
@ -1,65 +0,0 @@
|
||||||
function* pairwise(iter) {
|
|
||||||
let last;
|
|
||||||
let firstLoop = true;
|
|
||||||
for (const it of iter) {
|
|
||||||
if (firstLoop) {
|
|
||||||
firstLoop = false;
|
|
||||||
} else {
|
|
||||||
yield [last, it];
|
|
||||||
}
|
|
||||||
last = it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* enumerate(iter) {
|
|
||||||
let i = 0;
|
|
||||||
for (const it of iter) {
|
|
||||||
yield [i, it];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = (fn) =>
|
|
||||||
function* (iter) {
|
|
||||||
for (const [i, it] of enumerate(iter)) {
|
|
||||||
yield fn(it, i);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filter = (fn) =>
|
|
||||||
function* (iter) {
|
|
||||||
for (const it of iter) {
|
|
||||||
if (fn(it)) {
|
|
||||||
yield it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reduce = (fn, init) => (iter) => {
|
|
||||||
let acc = init;
|
|
||||||
for (const it of iter) {
|
|
||||||
acc = fn(acc, it);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scan = (fn) =>
|
|
||||||
function* (iter, init) {
|
|
||||||
let acc = init;
|
|
||||||
for (const it of iter) {
|
|
||||||
acc = fn(acc, it);
|
|
||||||
yield acc;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const flow = (...reducers) => (input) => reducers.reduce((c, fn) => fn(c), input);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
filter,
|
|
||||||
map,
|
|
||||||
enumerate,
|
|
||||||
pairwise,
|
|
||||||
flow,
|
|
||||||
reduce,
|
|
||||||
scan,
|
|
||||||
};
|
|
|
@ -1,30 +0,0 @@
|
||||||
const nodemailer = require('nodemailer');
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
module.exports = sendEmail;
|
|
||||||
|
|
||||||
async function sendEmail({ to, subject, html }) {
|
|
||||||
if (config.mail) {
|
|
||||||
const from = config.mail.from;
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: config.mail.smtp.host,
|
|
||||||
port: config.mail.smtp.port,
|
|
||||||
secure: !config.mail.smtp.starttls,
|
|
||||||
requiretls: config.mail.smtp.starttls,
|
|
||||||
auth: {
|
|
||||||
user: config.mail.smtp.username,
|
|
||||||
pass: config.mail.smtp.password,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await transporter.sendMail({ from, to, subject, html });
|
|
||||||
} else {
|
|
||||||
console.log(`========== E-Mail disabled, see contents below =========
|
|
||||||
To: ${to}
|
|
||||||
Subject: ${subject}
|
|
||||||
|
|
||||||
${html}
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
const wrapRoute = (fn) => async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
return await fn(req, res);
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = wrapRoute;
|
|
|
@ -1,23 +0,0 @@
|
||||||
module.exports = errorHandler;
|
|
||||||
|
|
||||||
function errorHandler(err, req, res, next) {
|
|
||||||
switch (true) {
|
|
||||||
case typeof err === 'string': {
|
|
||||||
// custom application error
|
|
||||||
const is404 = err.toLowerCase().endsWith('not found');
|
|
||||||
const statusCode = is404 ? 404 : 400;
|
|
||||||
return res.status(statusCode).json({ message: err });
|
|
||||||
}
|
|
||||||
|
|
||||||
case err.name === 'ValidationError':
|
|
||||||
// mongoose validation error
|
|
||||||
return res.status(400).json({ message: err.message });
|
|
||||||
|
|
||||||
case err.name === 'UnauthorizedError':
|
|
||||||
// jwt authentication error
|
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
|
||||||
|
|
||||||
default:
|
|
||||||
return res.status(500).json({ message: err.message });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
const validateRequest = (schema, property = 'body') => (req, res, next) => {
|
|
||||||
console.log('validateRequest');
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
abortEarly: false, // include all errors
|
|
||||||
allowUnknown: true, // ignore unknown props
|
|
||||||
stripUnknown: true, // remove unknown props
|
|
||||||
};
|
|
||||||
const { error, value } = schema.validate(req[property], options);
|
|
||||||
if (error) {
|
|
||||||
console.log('error: ', error);
|
|
||||||
next(`Validation error: ${error.details.map((x) => x.message).join(', ')}`);
|
|
||||||
} else {
|
|
||||||
req[property] = value;
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = validateRequest;
|
|
|
@ -1,146 +0,0 @@
|
||||||
const crypto = require('crypto');
|
|
||||||
const sendEmail = require('../_helpers/send-email');
|
|
||||||
const config = require('../config');
|
|
||||||
const { User } = require('../models');
|
|
||||||
|
|
||||||
const baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
register,
|
|
||||||
verifyEmail,
|
|
||||||
forgotPassword,
|
|
||||||
validateResetToken,
|
|
||||||
resetPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function register(params) {
|
|
||||||
const user = await User.findOne({ email: params.email });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
// send already registered error in email to prevent account enumeration
|
|
||||||
return await sendAlreadyRegisteredEmail(params.email);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = new User();
|
|
||||||
|
|
||||||
newUser.username = params.username;
|
|
||||||
newUser.email = params.email;
|
|
||||||
newUser.setPassword(params.password);
|
|
||||||
newUser.verificationToken = randomTokenString();
|
|
||||||
newUser.needsEmailValidation = true;
|
|
||||||
|
|
||||||
await newUser.save();
|
|
||||||
|
|
||||||
// send email
|
|
||||||
await sendVerificationEmail(newUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyEmail({ token }) {
|
|
||||||
const account = await User.findOne({ verificationToken: token });
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
throw Error('Verification failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
account.needsEmailValidation = false;
|
|
||||||
account.verificationToken = undefined;
|
|
||||||
await account.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function forgotPassword({ email }) {
|
|
||||||
const account = await User.findOne({ email });
|
|
||||||
|
|
||||||
console.log('forgotPassword', account, email);
|
|
||||||
|
|
||||||
// always return ok response to prevent email enumeration
|
|
||||||
if (!account) return;
|
|
||||||
|
|
||||||
// create reset token that expires after 24 hours
|
|
||||||
account.resetToken = {
|
|
||||||
token: randomTokenString(),
|
|
||||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
||||||
};
|
|
||||||
await account.save();
|
|
||||||
|
|
||||||
console.log('forgotPassword account saved', account);
|
|
||||||
|
|
||||||
// send email
|
|
||||||
await sendPasswordResetEmail(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateResetToken({ token }) {
|
|
||||||
const account = await User.findOne({
|
|
||||||
'resetToken.token': token,
|
|
||||||
'resetToken.expires': { $gt: Date.now() },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
throw Error('Invalid token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetPassword({ token, password }) {
|
|
||||||
const account = await User.findOne({
|
|
||||||
'resetToken.token': token,
|
|
||||||
'resetToken.expires': { $gt: Date.now() },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
throw Error('Invalid token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// update password and remove reset token
|
|
||||||
account.setPassword(password);
|
|
||||||
account.resetToken = undefined;
|
|
||||||
|
|
||||||
// Since password recovery happens through email, we can consider this a
|
|
||||||
// successful verification of the email address.
|
|
||||||
account.needsEmailValidation = false;
|
|
||||||
account.verificationToken = undefined;
|
|
||||||
|
|
||||||
await account.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomTokenString() {
|
|
||||||
return crypto.randomBytes(40).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendVerificationEmail(account) {
|
|
||||||
const verifyUrl = `${baseUrl}/verify-email?token=${account.verificationToken}`;
|
|
||||||
const html = [
|
|
||||||
'<h4>Verify Email</h4>',
|
|
||||||
'<p>Thanks for registering!</p>',
|
|
||||||
'<p>Please click the below link to verify your email address:</p>',
|
|
||||||
`<p><a href="${verifyUrl}">${verifyUrl}</a></p>`,
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
to: account.email,
|
|
||||||
subject: 'Sign-up Verification API - Verify Email',
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendAlreadyRegisteredEmail(email) {
|
|
||||||
const message = `<p>If you don't know your password please visit the <a href="${baseUrl}/forgot-password">forgot password</a> page.</p>`;
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
to: email,
|
|
||||||
subject: 'Sign-up Verification API - Email Already Registered',
|
|
||||||
html: `<h4>Email Already Registered</h4>
|
|
||||||
<p>Your email <strong>${email}</strong> is already registered.</p>
|
|
||||||
${message}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendPasswordResetEmail(account) {
|
|
||||||
const resetUrl = `${baseUrl}/reset-password?token=${account.resetToken.token}`;
|
|
||||||
const message = `<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
|
|
||||||
<p><a href="${resetUrl}">${resetUrl}</a></p>`;
|
|
||||||
await sendEmail({
|
|
||||||
to: account.email,
|
|
||||||
subject: 'Sign-up Verification API - Reset Password',
|
|
||||||
html: `<h4>Reset Password Email</h4>
|
|
||||||
${message}`,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const Joi = require('joi');
|
|
||||||
const wrapRoute = require('../_helpers/wrapRoute');
|
|
||||||
const validateRequest = require('../_middleware/validate-request');
|
|
||||||
const accountService = require('./account.service');
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/register',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
username: Joi.string().required(),
|
|
||||||
email: Joi.string().email().required(),
|
|
||||||
password: Joi.string().min(6).required(),
|
|
||||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.register(req.body, req.get('origin'));
|
|
||||||
res.json({ message: 'Registration successful, please check your email for verification instructions' });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/verify-email',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.verifyEmail(req.body);
|
|
||||||
res.json({ message: 'Verification successful, you can now login' });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/forgot-password',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
email: Joi.string().email().required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.forgotPassword(req.body, req.get('origin'));
|
|
||||||
res.json({ message: 'Please check your email for password reset instructions' });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/validate-reset-token',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.validateResetToken(req.body);
|
|
||||||
res.json({ message: 'Token is valid' });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/reset-password',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
password: Joi.string().min(6).required(),
|
|
||||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.resetPassword(req.body);
|
|
||||||
res.json({ message: 'Password reset successful, you can now login' });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,76 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const Joi = require('joi');
|
|
||||||
|
|
||||||
const configSchema = Joi.object({
|
|
||||||
jwtSecret: Joi.string().min(16).max(128).required(),
|
|
||||||
cookieSecret: Joi.string().min(16).max(128).required(),
|
|
||||||
|
|
||||||
imprintUrl: Joi.string(),
|
|
||||||
privacyPolicyUrl: Joi.string(),
|
|
||||||
|
|
||||||
baseUrl: Joi.string().required(),
|
|
||||||
mainFrontendUrl: Joi.string(), // optional
|
|
||||||
|
|
||||||
mail: Joi.alternatives().try(
|
|
||||||
Joi.object({
|
|
||||||
from: Joi.string().required(),
|
|
||||||
smtp: Joi.object({
|
|
||||||
host: Joi.string().required(),
|
|
||||||
port: Joi.number().default(465),
|
|
||||||
starttls: Joi.boolean().default(false),
|
|
||||||
username: Joi.string().required(),
|
|
||||||
password: Joi.string().required(),
|
|
||||||
}).required(),
|
|
||||||
}),
|
|
||||||
Joi.boolean().valid(false),
|
|
||||||
),
|
|
||||||
|
|
||||||
mongodb: Joi.object({
|
|
||||||
url: Joi.string().required(),
|
|
||||||
debug: Joi.boolean().default(process.env.NODE_ENV !== 'production'),
|
|
||||||
}).required(),
|
|
||||||
|
|
||||||
postgres: Joi.object({
|
|
||||||
url: Joi.string().required(),
|
|
||||||
}).required(),
|
|
||||||
|
|
||||||
redisUrl: Joi.string().required(),
|
|
||||||
|
|
||||||
oAuth2Clients: Joi.array()
|
|
||||||
.default([])
|
|
||||||
.items(
|
|
||||||
Joi.object({
|
|
||||||
title: Joi.string().required(),
|
|
||||||
clientId: Joi.string().required(),
|
|
||||||
validRedirectUris: Joi.array().required().items(Joi.string()),
|
|
||||||
|
|
||||||
// Set `refreshTokenExpirySeconds` to null to issue no refresh tokens. Set
|
|
||||||
// to a number of seconds to issue refresh tokens with that duration. No
|
|
||||||
// infinite tokens are ever issued, set to big number to simulate that.
|
|
||||||
refreshTokenExpirySeconds: Joi.number()
|
|
||||||
.default(null)
|
|
||||||
.min(1) // 0 would make no sense, use `null` to issue no token
|
|
||||||
.max(1000 * 24 * 60 * 60), // 1000 days, nearly 3 years
|
|
||||||
|
|
||||||
// Set to a scope which cannot be exceeded when requesting client tokens.
|
|
||||||
// Clients must manually request a scope that is smaller or equal to this
|
|
||||||
// scope to get a valid response. Scopes are not automatically truncated.
|
|
||||||
// Leave empty or set to `"*"` for unlimited scopes in this client.
|
|
||||||
maxScope: Joi.string().required(),
|
|
||||||
|
|
||||||
autoAccept: Joi.boolean().optional(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}).required();
|
|
||||||
|
|
||||||
const configFiles = [
|
|
||||||
process.env.CONFIG_FILE,
|
|
||||||
process.env.NODE_ENV === 'production' ? 'config.prod.json' : 'config.dev.json',
|
|
||||||
'config.json',
|
|
||||||
].filter((x) => x && fs.existsSync(x));
|
|
||||||
|
|
||||||
if (!configFiles.length) {
|
|
||||||
throw new Error('No config file found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Joi.attempt(JSON.parse(fs.readFileSync(configFiles[0], 'utf8')), configSchema);
|
|
|
@ -1,13 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const config = require('./config')
|
|
||||||
|
|
||||||
mongoose.connect(config.mongodb.url);
|
|
||||||
mongoose.set('debug', config.mongodb.debug);
|
|
||||||
|
|
||||||
require('./models/User');
|
|
||||||
require('./models/Track');
|
|
||||||
require('./models/Comment');
|
|
||||||
require('./passport');
|
|
||||||
|
|
||||||
module.exports = mongoose;
|
|
|
@ -1,87 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const express = require('express');
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const session = require('express-session');
|
|
||||||
const cors = require('cors');
|
|
||||||
const errorhandler = require('errorhandler');
|
|
||||||
const passport = require('passport');
|
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
require('./passport');
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
// Create global app object
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
// Express configuration
|
|
||||||
app.set('views', path.join(__dirname, '..', 'views'));
|
|
||||||
app.set('view engine', 'pug');
|
|
||||||
|
|
||||||
// Normal express config defaults
|
|
||||||
app.use(require('morgan')('dev'));
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: false }));
|
|
||||||
|
|
||||||
app.use(require('method-override')());
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
app.use('/semantic-ui', express.static(path.join(__dirname, '..', 'node_modules', 'semantic-ui-css')));
|
|
||||||
|
|
||||||
app.use(session({ secret: config.cookieSecret, cookie: { maxAge: 10 * 60 * 1000 }, resave: false, saveUninitialized: false }));
|
|
||||||
app.use(passport.initialize());
|
|
||||||
app.use(passport.session());
|
|
||||||
|
|
||||||
if (!isProduction) {
|
|
||||||
app.use(errorhandler());
|
|
||||||
}
|
|
||||||
|
|
||||||
require('./db');
|
|
||||||
require('./models');
|
|
||||||
|
|
||||||
app.use(require('./routes'));
|
|
||||||
|
|
||||||
/// catch 404 and forward to error handler
|
|
||||||
app.use(function (req, res, next) {
|
|
||||||
const err = new Error('Not Found');
|
|
||||||
err.status = 404;
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
/// error handlers
|
|
||||||
|
|
||||||
// development error handler
|
|
||||||
// will print stacktrace
|
|
||||||
if (!isProduction) {
|
|
||||||
app.use(function (err, req, res, next) {
|
|
||||||
console.log(err.stack);
|
|
||||||
|
|
||||||
res.status(err.status || 500);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
errors: {
|
|
||||||
message: err.message,
|
|
||||||
error: err,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// production error handler
|
|
||||||
// no stacktraces leaked to user
|
|
||||||
app.use(function (err, req, res, next) {
|
|
||||||
res.status(err.status || 500);
|
|
||||||
res.json({
|
|
||||||
errors: {
|
|
||||||
message: err.message,
|
|
||||||
error: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// finally, let's start our server...
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log('Listening on port ' + port);
|
|
||||||
});
|
|
|
@ -1,357 +0,0 @@
|
||||||
const TEST_ROWS = [
|
|
||||||
'Date;Time;Latitude;Longitude;Course;Speed;Right;Left;Confirmed;insidePrivacyArea;',
|
|
||||||
'12.07.2020;09:02:59;0.000000;0.000000;0.000;0.0000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:02:59;0.000000;0.000000;0.000;0.0000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:00;0.000000;0.000000;0.000;0.0000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:01;48.722205;9.270218;0.000;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:02;48.722206;9.270219;0.000;0.5741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:03;48.722204;9.270221;0.000;0.5371;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:04;48.722198;9.270229;0.000;0.7593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:05;48.722188;9.270241;0.000;0.5556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:06;48.722174;9.270259;0.000;0.4815;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:07;48.722158;9.270278;0.000;0.3704;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:08;48.722146;9.270293;0.000;0.5741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:09;48.722138;9.270305;0.000;1.2594;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:10;48.722129;9.270318;0.000;1.5557;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:11;48.722122;9.270329;0.000;1.5372;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:12;48.722115;9.270339;0.000;0.4630;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:13;48.722107;9.270350;0.000;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:14;48.722101;9.270357;0.000;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:15;48.722092;9.270367;0.000;0.8149;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:16;48.722084;9.270377;0.000;1.2223;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:17;48.722076;9.270385;0.000;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:18;48.722070;9.270391;0.000;1.4816;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:19;48.722070;9.270392;0.000;1.0927;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:20;48.722066;9.270395;0.000;1.6668;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:21;48.722068;9.270391;0.000;2.0742;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:22;48.722064;9.270396;0.000;1.6853;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:23;48.722060;9.270401;0.000;1.0927;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:24;48.722056;9.270406;0.000;0.9445;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:25;48.722052;9.270411;0.000;0.7964;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:26;48.722047;9.270416;0.000;0.6482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:27;48.722042;9.270419;0.000;1.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:28;48.722031;9.270433;0.000;2.0372;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:29;48.722031;9.270432;0.000;2.4261;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:30;48.722029;9.270433;0.000;0.8704;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:31;48.722029;9.270433;0.000;1.8150;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:32;48.722024;9.270439;0.000;1.2223;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:33;48.722025;9.270439;0.000;0.3889;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:34;48.722022;9.270440;0.000;0.3519;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:35;48.722020;9.270445;0.000;0.9445;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:36;48.722018;9.270447;0.000;0.9260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:37;48.722020;9.270444;0.000;0.9075;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:38;48.722021;9.270443;0.000;1.9261;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:39;48.722018;9.270447;0.000;0.3334;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:40;48.722020;9.270445;0.000;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:41;48.722023;9.270440;0.000;1.2594;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:42;48.722023;9.270442;0.000;0.5000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:43;48.722025;9.270440;0.000;0.6852;220;255;0;0;',
|
|
||||||
'12.07.2020;09:03:44;48.722023;9.270441;0.000;0.8519;199;255;0;0;',
|
|
||||||
'12.07.2020;09:03:45;48.722026;9.270438;0.000;1.4075;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:46;48.722029;9.270436;0.000;0.5371;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:47;48.722028;9.270435;0.000;0.8334;97;255;0;0;',
|
|
||||||
'12.07.2020;09:03:48;48.722029;9.270435;0.000;0.3704;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:49;48.722029;9.270436;0.000;1.1112;96;255;0;0;',
|
|
||||||
'12.07.2020;09:03:50;48.722029;9.270435;0.000;1.8890;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:51;48.722034;9.270429;0.000;1.0186;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:52;48.721942;9.270529;128.450;5.2226;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;79;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;178;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;89;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;156;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;168;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;181;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;176;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;186;0;0;',
|
|
||||||
'12.07.2020;09:03:53;48.721929;9.270546;128.450;1.3520;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:10;48.721896;9.270602;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:11;48.721894;9.270609;916.230;0.0926;255;192;0;0;',
|
|
||||||
'12.07.2020;09:04:12;48.721892;9.270616;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:13;48.721890;9.270623;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:14;48.721888;9.270629;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:15;48.721886;9.270635;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:16;48.721883;9.270640;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:17;48.721881;9.270644;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:18;48.721879;9.270649;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:19;48.721877;9.270653;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:20;48.721876;9.270657;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:21;48.721874;9.270658;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:22;48.721873;9.270659;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:23;48.721872;9.270661;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:24;48.721871;9.270661;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:25;48.721870;9.270660;916.230;0.3334;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:26;48.721869;9.270658;916.230;0.5000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:27;48.721866;9.270660;916.230;1.6853;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:28;48.721866;9.270659;916.230;0.8704;255;198;0;0;',
|
|
||||||
'12.07.2020;09:04:29;48.721867;9.270659;916.230;0.5741;255;196;0;0;',
|
|
||||||
'12.07.2020;09:04:30;48.721867;9.270660;916.230;0.3148;255;196;0;0;',
|
|
||||||
'12.07.2020;09:04:31;48.721867;9.270659;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:32;48.721866;9.270659;916.230;0.0556;255;199;0;0;',
|
|
||||||
'12.07.2020;09:04:33;48.721867;9.270656;916.230;0.1482;255;199;0;0;',
|
|
||||||
'12.07.2020;09:04:34;48.721867;9.270654;916.230;0.0370;255;198;0;0;',
|
|
||||||
'12.07.2020;09:04:35;48.721867;9.270653;916.230;0.1296;255;198;0;0;',
|
|
||||||
'12.07.2020;09:04:36;48.721867;9.270651;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:37;48.721867;9.270650;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:38;48.721868;9.270650;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:39;48.721868;9.270649;916.230;0.1667;255;201;0;0;',
|
|
||||||
'12.07.2020;09:04:40;48.721868;9.270647;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:41;48.721869;9.270644;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:42;48.721869;9.270641;916.230;0.0185;255;198;0;0;',
|
|
||||||
'12.07.2020;09:04:43;48.721870;9.270638;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:44;48.721870;9.270635;916.230;0.0370;255;199;0;0;',
|
|
||||||
'12.07.2020;09:04:45;48.721871;9.270632;916.230;0.1482;255;204;0;0;',
|
|
||||||
'12.07.2020;09:04:46;48.721871;9.270630;916.230;0.0185;255;201;0;0;',
|
|
||||||
'12.07.2020;09:04:47;48.721873;9.270630;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:48;48.721873;9.270629;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:49;48.721874;9.270628;916.230;0.4074;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:50;48.721875;9.270627;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:51;48.721876;9.270625;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:52;48.721877;9.270623;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:53;48.721877;9.270622;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:54;48.721879;9.270621;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:55;48.721881;9.270618;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:56;48.721883;9.270615;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:57;48.721884;9.270612;916.230;0.2778;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:58;48.721885;9.270609;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:04:59;48.721886;9.270606;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:00;48.721888;9.270602;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:01;48.721889;9.270598;916.230;0.1111;255;191;0;0;',
|
|
||||||
'12.07.2020;09:05:02;48.721890;9.270595;916.230;0.1482;255;193;0;0;',
|
|
||||||
'12.07.2020;09:05:03;48.721891;9.270593;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:04;48.721891;9.270591;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:05;48.721891;9.270589;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:06;48.721891;9.270587;916.230;0.3519;255;199;0;0;',
|
|
||||||
'12.07.2020;09:05:07;48.721891;9.270586;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:08;48.721891;9.270588;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:09;48.721890;9.270589;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:10;48.721889;9.270589;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:11;48.721888;9.270589;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:12;48.721887;9.270589;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:13;48.721886;9.270590;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:14;48.721885;9.270591;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:15;48.721885;9.270592;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:16;48.721885;9.270596;916.230;0.5556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:17;48.721885;9.270598;916.230;0.3519;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:18;48.721884;9.270600;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:19;48.721882;9.270600;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:20;48.721881;9.270602;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:21;48.721879;9.270603;916.230;0.0185;255;206;0;0;',
|
|
||||||
'12.07.2020;09:05:22;48.721878;9.270605;916.230;0.0556;255;203;0;0;',
|
|
||||||
'12.07.2020;09:05:23;48.721876;9.270606;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:24;48.721874;9.270605;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:25;48.721873;9.270605;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:26;48.721872;9.270605;916.230;0.1296;255;209;0;0;',
|
|
||||||
'12.07.2020;09:05:27;48.721870;9.270606;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:28;48.721869;9.270608;916.230;0.1111;255;206;0;0;',
|
|
||||||
'12.07.2020;09:05:29;48.721868;9.270610;916.230;0.3148;255;209;0;0;',
|
|
||||||
'12.07.2020;09:05:30;48.721867;9.270610;916.230;0.2593;255;208;0;0;',
|
|
||||||
'12.07.2020;09:05:31;48.721866;9.270611;916.230;0.0556;255;210;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:32;48.721866;9.270612;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:44;48.721855;9.270602;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:44;48.721855;9.270602;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:46;48.721854;9.270602;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:46;48.721854;9.270602;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:48;48.721852;9.270606;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:49;48.721851;9.270611;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:50;48.721851;9.270615;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:51;48.721851;9.270616;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:52;48.721851;9.270617;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:53;48.721852;9.270617;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:54;48.721853;9.270616;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:55;48.721855;9.270613;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:56;48.721858;9.270609;916.230;0.6482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:57;48.721860;9.270606;916.230;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:58;48.721864;9.270601;916.230;0.6297;255;255;0;0;',
|
|
||||||
'12.07.2020;09:05:59;48.721867;9.270595;916.230;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:00;48.721872;9.270589;916.230;0.5000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:01;48.721875;9.270584;916.230;0.2593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:02;48.721880;9.270578;916.230;0.5186;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:03;48.721883;9.270574;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:04;48.721886;9.270570;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:05;48.721890;9.270565;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:06;48.721893;9.270562;916.230;0.2593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:07;48.721893;9.270560;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:08;48.721894;9.270559;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:09;48.721894;9.270557;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:07;48.721896;9.270556;916.230;0.2778;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:08;48.721896;9.270556;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:09;48.721895;9.270557;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:10;48.721894;9.270559;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:11;48.721892;9.270560;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:12;48.721891;9.270561;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:13;48.721892;9.270562;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:14;48.721891;9.270564;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:15;48.721889;9.270566;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:16;48.721888;9.270568;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:17;48.721888;9.270570;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:18;48.721888;9.270572;916.230;0.4630;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:19;48.721887;9.270573;916.230;0.4815;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:20;48.721886;9.270574;916.230;0.3334;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:21;48.721885;9.270576;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:22;48.721884;9.270579;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:23;48.721882;9.270581;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:24;48.721881;9.270584;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:25;48.721880;9.270589;916.230;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:26;48.721879;9.270596;916.230;0.3519;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:27;48.721878;9.270602;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:28;48.721876;9.270601;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:29;48.721874;9.270603;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:30;48.721873;9.270607;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:31;48.721872;9.270614;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:32;48.721870;9.270613;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:33;48.721869;9.270614;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:34;48.721868;9.270616;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:35;48.721867;9.270617;916.230;0.2593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:36;48.721867;9.270618;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:37;48.721867;9.270618;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:38;48.721867;9.270616;916.230;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:39;48.721867;9.270613;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:40;48.721867;9.270607;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:41;48.721866;9.270601;916.230;0.5186;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:42;48.721866;9.270593;916.230;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:43;48.721866;9.270587;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:44;48.721866;9.270581;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:45;48.721866;9.270576;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:46;48.721866;9.270567;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:47;48.721866;9.270558;916.230;0.4074;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:48;48.721866;9.270550;916.230;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:49;48.721866;9.270543;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:50;48.721867;9.270537;916.230;0.2778;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:51;48.721867;9.270532;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:52;48.721867;9.270526;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:53;48.721868;9.270522;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:54;48.721868;9.270517;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:55;48.721869;9.270512;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:56;48.721869;9.270506;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:57;48.721869;9.270503;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:58;48.721870;9.270500;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:06:59;48.721870;9.270497;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:00;48.721871;9.270494;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:01;48.721871;9.270493;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:02;48.721871;9.270492;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:03;48.721872;9.270490;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:04;48.721873;9.270489;916.230;0.6667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:05;48.721873;9.270487;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:06;48.721873;9.270486;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:07;48.721873;9.270486;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:08;48.721873;9.270485;916.230;0.1852;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:09;48.721873;9.270485;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:10;48.721872;9.270485;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:11;48.721870;9.270486;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:12;48.721869;9.270489;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:13;48.721867;9.270492;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:14;48.721865;9.270494;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:15;48.721863;9.270495;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:16;48.721861;9.270497;916.230;0.0000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:17;48.721860;9.270496;916.230;0.4074;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:18;48.721859;9.270495;916.230;0.4445;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:19;48.721857;9.270496;916.230;0.3889;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:20;48.721856;9.270496;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:21;48.721854;9.270494;916.230;0.7593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:22;48.721851;9.270496;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:23;48.721850;9.270497;916.230;0.1667;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:24;48.721848;9.270501;916.230;0.4074;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:25;48.721847;9.270504;916.230;0.4074;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:26;48.721846;9.270505;916.230;0.2037;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:27;48.721844;9.270508;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:28;48.721843;9.270507;916.230;0.4630;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:29;48.721842;9.270509;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:30;48.721841;9.270512;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:31;48.721840;9.270515;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:32;48.721839;9.270517;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:33;48.721838;9.270522;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:34;48.721838;9.270527;916.230;0.3889;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:35;48.721837;9.270530;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:36;48.721836;9.270532;916.230;0.1111;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:37;48.721835;9.270536;916.230;0.6112;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:38;48.721835;9.270541;916.230;1.1668;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:39;48.721835;9.270543;916.230;0.3889;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:40;48.721834;9.270545;916.230;0.5000;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:41;48.721834;9.270544;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:42;48.721834;9.270545;916.230;0.7593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:43;48.721834;9.270545;916.230;0.8890;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:44;48.721834;9.270543;916.230;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:45;48.721834;9.270541;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:46;48.721834;9.270540;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:47;48.721835;9.270538;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:48;48.721835;9.270535;916.230;0.0556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:49;48.721835;9.270534;916.230;0.8890;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:50;48.721835;9.270534;916.230;0.5926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:51;48.721835;9.270534;916.230;0.7593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:52;48.721836;9.270533;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:53;48.721836;9.270531;916.230;0.0741;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:54;48.721836;9.270529;916.230;0.3889;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:55;48.721836;9.270530;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:56;48.721836;9.270530;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:57;48.721837;9.270531;916.230;0.0185;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:58;48.721837;9.270530;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:07:59;48.721838;9.270526;916.230;0.3519;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:00;48.721838;9.270521;916.230;0.4260;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:01;48.721839;9.270522;916.230;0.5556;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:02;48.721840;9.270524;916.230;0.3519;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:03;48.721842;9.270525;916.230;0.2963;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:04;48.721843;9.270525;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:05;48.721844;9.270524;916.230;0.2222;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:06;48.721846;9.270522;916.230;0.3704;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:07;48.721847;9.270519;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:08;48.721848;9.270516;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:09;48.721849;9.270514;916.230;0.1296;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:10;48.721850;9.270512;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:11;48.721851;9.270513;916.230;0.3334;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:12;48.721851;9.270512;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:13;48.721851;9.270512;916.230;0.2593;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:14;48.721852;9.270511;916.230;0.0926;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:15;48.721853;9.270512;916.230;0.0370;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:16;48.721852;9.270515;916.230;0.5371;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:17;48.721853;9.270517;916.230;0.1482;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:18;48.721854;9.270519;916.230;0.3148;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:19;48.721855;9.270520;916.230;0.2408;255;255;0;0;',
|
|
||||||
'12.07.2020;09:08:20;48.721856;9.270523;916.230;0.3704;255;255;0;0;',
|
|
||||||
];
|
|
||||||
|
|
||||||
const test1 = TEST_ROWS.join('$');
|
|
||||||
|
|
||||||
const test2 = `OBSFirmwareVersion=v0.3.999&OBSDataFormat=2&DataPerMeasurement=3&MaximumMeasurementsPerLine=60&OffsetLeft=30&OffsetRight=30&NumberOfDefinedPrivacyAreas=3&PrivacyLevelApplied=AbsolutePrivacy&MaximumValidFlightTimeMicroseconds=18560&DistanceSensorsUsed=HC-SR04/JSN-SR04T&DeviceId=ECEC&OBSUserID=32423432342234
|
|
||||||
Date;Time;Millis;Comment;Latitude;Longitude;Altitude;Course;Speed;HDOP;Satellites;BatteryLevel;Left;Right;Confirmed;Marked;Invalid;InsidePrivacyArea;Factor;Measurements;Tms1;Lus1;Rus1;Tms2;Lus2;Rus2;Tms3;Lus3;Rus3;Tms4;Lus4;Rus4;Tms5;Lus5;Rus5;Tms6;Lus6;Rus6;Tms7;Lus7;Rus7;Tms8;Lus8;Rus8;Tms9;Lus9;Rus9;Tms10;Lus10;Rus10;Tms11;Lus11;Rus11;Tms12;Lus12;Rus12;Tms13;Lus13;Rus13;Tms14;Lus14;Rus14;Tms15;Lus15;Rus15;Tms16;Lus16;Rus16;Tms17;Lus17;Rus17;Tms18;Lus18;Rus18;Tms19;Lus19;Rus19;Tms20;Lus20;Rus20;Tms21;Lus21;Rus21;Tms22;Lus22;Rus22;Tms23;Lus23;Rus23;Tms24;Lus24;Rus24;Tms25;Lus25;Rus25;Tms26;Lus26;Rus26;Tms27;Lus27;Rus27;Tms28;Lus28;Rus28;Tms29;Lus29;Rus29;Tms30;Lus30;Rus30;Tms31;Lus31;Rus31;Tms32;Lus32;Rus32;Tms33;Lus33;Rus33;Tms34;Lus34;Rus34;Tms35;Lus35;Rus35;Tms36;Lus36;Rus36;Tms37;Lus37;Rus37;Tms38;Lus38;Rus38;Tms39;Lus39;Rus39;Tms40;Lus40;Rus40;Tms41;Lus41;Rus41;Tms42;Lus42;Rus42;Tms43;Lus43;Rus43;Tms44;Lus44;Rus44;Tms45;Lus45;Rus45;Tms46;Lus46;Rus46;Tms47;Lus47;Rus47;Tms48;Lus48;Rus48;Tms49;Lus49;Rus49;Tms50;Lus50;Rus50;Tms51;Lus51;Rus51;Tms52;Lus52;Rus52;Tms53;Lus53;Rus53;Tms54;Lus54;Rus54;Tms55;Lus55;Rus55;Tms56;Lus56;Rus56;Tms57;Lus57;Rus57;Tms58;Lus58;Rus58;Tms59;Lus59;Rus59;Tms60;Lus60;Rus60
|
|
||||||
18.11.2020;16:05:59;1265034;;48.723224;9.094103;495.3;189.86;3.2;1.01;7;3.74;770;;0;0;58;54;0;6231;;16;;;36;6350;;52;;;72;6263;;87;;;107;6828;;122;;;143;6836;;158;;;178;6936;;193;;;213;7094;;228;;;248;6822;;263;;;284;7019;;299;;;319;6942;;334;;;354;7110;;370;;;390;7203;;405;;;425;7758;;440;;;461;7266;;476;;;496;7499;;511;;;531;7328;;546;;;567;7354;;582;;;602;7397;;617;;;637;;;664;;;684;16615;;708;;;728;9161;;745;;;765;10238;;783;;;802;8525;;818;;;839;7756;;854;;;875;7580;;890;;;910;7926;;925;;;945;7624;;960;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:00;1266041;DEVELOP: GPSMessages: 2587 GPS crc errors: 0;48.723205;9.0941;495.4;189.86;2.87;1.01;7;3.74;1020;;0;0;58;53;0;8012;;27;;;47;7999;;62;;;83;7660;;98;;;118;7698;;133;;;158;1252;;169;;;194;1146;;204;;;229;1173;;239;;;264;1173;;274;;;300;1147;;310;;;335;7943;;352;;;371;8713;;387;;;407;8005;;423;;;443;8021;;458;;;478;;;505;;;525;8111;;541;;;560;8074;;576;;;596;8254;;612;;;632;8514;;647;;;667;8195;;682;;;703;8094;;718;;;738;8123;;754;;;774;8330;;789;;;810;8966;;826;;;846;9066;;862;;;882;10553;;899;;;920;8345;;935;;;955;9219;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:01;1267037;DEVELOP: Mem: 45k Buffer: 4k last write time: 58;48.723197;9.094089;495.7;189.86;2.93;1.01;7;3.74;1090;;0;0;58;53;0;8164;;18;;;39;8184;;53;;;74;16305;;98;;;118;8658;;135;;;155;8198;;170;;;190;8133;;205;;;226;8536;;241;;;261;8676;;276;;;296;8516;;314;;;334;8114;;350;;;370;8294;;385;;;405;8751;;422;;;441;8163;;457;;;478;8062;;493;;;513;8093;;528;;;549;8060;;564;;;584;8085;;599;;;619;8071;;634;;;655;8262;;671;;;690;8746;;707;;;726;9116;;742;;;762;;;789;;;808;8121;;825;;;845;8113;;860;;;881;8129;;896;;;916;8096;;932;;;952;10617;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:02;1268027;;48.723185;9.094076;496.1;189.86;3.02;1.01;7;3.74;980;;0;0;58;55;0;8173;;18;;;37;8535;;53;;;73;8435;;88;;;109;8592;;124;;;144;8012;;159;;;180;8037;;195;;;215;7975;;230;;;250;7970;;265;;;286;7850;;301;;;321;7861;;336;;;356;7826;;371;;;392;8097;;407;;;427;8467;;443;;;463;7763;;478;;;498;7687;;513;;;534;7950;;549;;;569;7806;;584;;;604;8253;;620;;;640;7753;;656;;;676;8188;;692;;;711;7533;;727;;;747;7791;;763;;;783;7460;;798;;;825;9827;;843;;;863;7432;;878;;;904;7646;;919;;;939;7538;;955;;;974;7508;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:03;1269096;;48.723177;9.094068;496.2;189.86;3;1.01;7;3.74;920;;0;0;58;51;0;7218;;19;;;38;8144;;54;;;74;7463;;89;;;110;7856;;125;;;145;7869;;161;;;181;7422;;196;;;216;7934;;232;;;252;7363;;267;;;293;7297;;307;;;332;8105;;348;;;367;7468;;383;;;403;7213;;418;;;439;7172;;454;;;478;7184;;489;;;514;7312;;528;;;550;7175;;565;;;585;7180;;600;;;620;7013;;635;;;655;7154;;670;;;691;7240;;706;;;726;7075;;741;;;761;7133;;776;;;801;7511;;815;;;836;7639;;851;;;872;8891;;888;;;908;7070;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:04;1270033;;48.723167;9.094056;496.6;189.86;3.19;1.01;7;3.74;870;;0;0;58;53;0;7617;;19;;;39;6812;;55;;;80;1173;;90;;;116;8173;;133;;;152;7431;;168;;;188;7197;;203;;;223;6984;;238;;;259;7218;;274;;;294;6881;;309;;;329;7111;;344;;;365;7500;;380;;;400;7462;;415;;;435;7094;;450;;;471;6820;;486;;;506;7147;;521;;;541;9156;;558;;;578;6961;;594;;;614;;;641;;;660;7176;;676;;;696;7177;;712;;;732;7199;;747;;;767;7218;;782;;;802;7360;;817;;;838;;;865;;;884;;;904;;;924;;;943;;;962;7252;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:05;1271032;;48.723153;9.094046;496.5;189.86;3.48;1.01;7;3.74;940;;0;0;58;54;0;7295;;14;;;35;7183;;50;;;71;7283;;85;;;106;8957;;122;;;142;8178;;158;;;178;7814;;194;;;213;7495;;229;;;249;7713;;265;;;285;7305;;300;;;320;7654;;335;;;356;7687;;371;;;391;7634;;406;;;426;7167;;441;;;461;;;488;;;508;7245;;524;;;544;7283;;559;;;580;7150;;595;;;615;7194;;630;;;650;7410;;665;;;686;7670;;702;;;721;7421;;737;;;757;7588;;772;;;792;7452;;809;;;828;8162;;844;;;865;9078;;881;;;901;7563;;917;;;936;7775;;952;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:06;1272031;;48.723146;9.094036;496.5;189.86;2.44;1.01;7;3.74;1000;;0;0;58;54;0;8193;;18;;;39;7629;;54;;;74;;;102;;;121;7778;;137;;;157;7773;;172;;;193;7922;;208;;;228;7706;;243;;;263;8881;;280;;;299;7776;;315;;;334;7797;;350;;;370;8683;;386;;;406;7863;;422;;;441;7901;;457;;;477;7747;;492;;;513;8246;;529;;;549;7756;;564;;;585;7667;;600;;;620;7657;;635;;;655;;;682;;;702;8193;;719;;;738;7751;;754;;;774;7731;;789;;;809;8109;;825;;;845;7623;;860;;;880;7883;;895;;;916;7579;;931;;;951;7514;;966;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:07;1273044;;48.723134;9.094026;496.7;189.86;3.44;1.01;7;3.74;990;;0;0;58;53;0;7543;;15;;;37;7535;;52;;;72;9628;;90;;;109;8166;;125;;;146;7469;;161;;;181;7923;;197;;;216;7651;;232;;;252;7594;;267;;;288;7796;;303;;;323;7960;;338;;;359;7862;;373;;;394;7633;;409;;;429;7926;;444;;;465;7661;;479;;;500;7546;;515;;;535;7522;;550;;;570;8461;;587;;;606;7520;;622;;;643;;;668;;;688;7495;;704;;;723;7672;;739;;;759;7964;;774;;;795;8725;;811;;;831;7366;;847;;;867;7586;;882;;;902;8634;;919;;;938;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:08;1274030;;48.723126;9.094013;496.7;230.11;3.54;1.01;7;3.74;850;;0;0;58;53;0;7452;;29;;;49;7446;;65;;;91;1147;;102;;;127;7517;;143;;;163;7411;;178;;;204;1148;;214;;;240;7282;;256;;;281;1201;;291;;;316;1144;;326;;;351;1173;;362;;;386;6718;;401;;;422;7303;;437;;;461;7621;;476;;;497;7557;;511;;;532;7451;;547;;;567;7658;;583;;;603;7534;;618;;;638;7306;;653;;;673;7222;;688;;;709;7169;;724;;;744;7115;;759;;;779;7277;;794;;;815;;;841;;;861;;;881;;;900;7403;;916;;;936;7356;;951;;;972;7030;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:09;1275038;;48.723121;9.093994;496.9;237.39;3.33;1.01;7;3.74;730;;0;0;58;54;0;7327;;16;;;36;6876;;52;;;72;6953;;87;;;107;7261;;122;;;142;6702;;158;;;178;7286;;193;;;213;6605;;228;;;249;7168;;264;;;284;6641;;299;;;324;7059;;339;;;359;7568;;374;;;394;6476;;409;;;430;6589;;445;;;470;1174;;480;;;505;1173;;515;;;541;1175;;551;;;576;1149;;586;;;611;6222;;626;;;647;6722;;661;;;687;5939;;700;;;723;5989;;735;;;760;6144;;773;;;795;6019;;808;;;830;6306;;844;;;866;6169;;879;;;901;6288;;914;;;936;9882;;954;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:10;1276038;;48.723117;9.093979;497.4;247.62;2.96;1.01;7;3.74;7;69;0;;0;0;58;52;0;;;30;;;50;;;69;;;89;;;109;;6187;124;9730;;144;;6203;160;14558;;182;;6178;195;;;222;;6233;235;;;257;;6323;275;;;295;;6379;311;;;331;;6371;346;8588;;366;;6330;381;2150;;401;;6275;417;1200;;437;;6184;461;;;488;;6033;505;;;525;;5943;543;2550;;561;;5872;579;2563;;596;;5844;614;1225;;631;;5835;650;1173;;667;;5799;685;2243;;702;;5804;720;2275;;737;;5798;759;;;785;;5854;805;;;825;;5984;840;;;860;;5979;875;;;895;;6027;911;7850;;931;;6001;946;13531;;969;;5957;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:11;1277041;;48.723117;9.093965;497.5;247.62;2.74;1.01;7;3.74;143;72;0;;0;0;58;48;0;;;30;;5966;43;;;65;;5940;78;12209;;101;;5923;113;;;140;;7918;155;18175;;182;;6159;199;;;226;;6047;239;;;261;;6283;274;;;297;;;328;;;348;;6015;364;;;384;;;418;1174;;428;;6166;453;;;480;;6265;498;;;518;;6241;536;9449;;553;;6311;571;17498;;597;;6394;611;;;638;;6380;652;;;673;;6408;687;;;708;;7059;722;7897;;744;;7059;759;10810;;779;;6459;794;;;822;;6680;841;15140;;864;;;891;6403;;906;;9053;930;10084;;948;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:12;1278045;;48.723109;9.093963;498;247.62;2.17;1.79;7;3.74;143;76;0;;0;0;58;48;0;;;30;;6485;52;;;71;;6321;93;;;113;;6283;128;;;148;;6319;164;10355;;184;;6232;199;17561;;225;;6259;238;;;265;;;292;16478;;317;;;344;8916;;361;;6160;379;;;406;;6242;426;;;445;;6318;461;;;481;;6172;496;;;516;;6271;534;;;554;;6184;571;10174;;590;;6204;607;14878;;630;;6333;643;;;670;;6332;683;;;705;;6231;718;;;740;;6227;753;;;776;;;803;;;822;;6469;844;;;864;;6215;879;;;899;;;927;;;946;;6326;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:13;1279029;;48.723109;9.093963;498;247.62;0;1.79;7;3.74;116;79;0;;0;0;58;47;0;;;20;;6783;41;;;61;;6343;81;;;101;;6506;121;11871;;142;;6364;162;16368;;185;;6365;197;;;224;;;250;15312;;274;;6698;295;12786;;315;;6428;330;;;357;;6556;375;17429;;401;;6426;418;16587;;444;;6539;462;;;488;;;515;18278;;542;;6507;556;;;584;;6506;593;;;619;;6774;632;;;654;;6775;668;;;690;;;717;;;737;;;764;;;784;;6708;799;;;819;;6908;840;8503;;856;;;882;1202;;893;;6729;917;13601;;939;;;974;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:14;1280049;;48.723109;9.093963;498;247.62;0;1.1;7;3.74;121;92;0;;0;0;58;45;0;;;33;;7087;56;;;75;;7170;94;;;113;;7158;129;;;149;;7575;164;;;184;;7233;204;13424;;226;;7616;248;18289;;282;;;315;;;342;;7353;357;;;377;;7521;400;;;419;;7510;435;;;455;;;481;;;500;;7484;516;12940;;537;;;571;8777;;588;;;615;11659;;634;;;660;1174;;670;;;696;;;723;;;742;;;762;;7640;778;;;797;;7980;819;;;839;;8759;855;;;875;;7752;890;11740;;910;;7612;929;12291;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:15;1281028;;48.723109;9.093963;498;247.62;0;1.01;7;3.74;133;95;0;;0;0;58;48;0;;;30;;7564;49;12590;;70;;7576;84;;;111;;7584;120;;;146;;7794;161;;;182;;7826;196;;;217;;;244;;;264;;8057;286;;;305;;7525;321;8033;;340;;7657;360;11295;;380;;;407;9480;;423;;;451;12842;;472;;7540;487;16790;;512;;;538;1175;;549;;;574;1175;;584;;7788;609;1175;;619;;;647;;;673;;7354;697;;;716;;7287;738;;;758;;7454;773;;;793;;7286;812;9244;;829;;7388;848;17471;;874;;7246;893;;;919;;;952;8724;;968;;7324;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
18.11.2020;16:06:16;1282037;;48.723109;9.093963;498;247.62;0;1.01;7;3.74;5;89;20;;0;0;58;49;0;;;31;;7290;44;;;66;;7277;80;;;101;;;128;;;148;;7213;164;;;183;;6901;202;10902;;223;;7060;242;2257;;258;;7057;277;2124;;293;;7045;313;1201;;328;;;361;2137;;371;;6931;396;2055;;407;;6910;432;1201;;442;;;468;2042;;478;;6961;503;1201;;513;;;548;12669;;568;;6909;590;;;617;;7063;636;;;656;;7148;672;;;691;;6777;707;;;727;;6903;747;11631;;767;;;793;1174;;803;;7283;828;;;856;;;889;9154;;908;;7489;929;9129;;943;;7430;965;14679;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const test3 = `Date;Time;Millis;Comment;Latitude;Longitude;Altitude;Course;Speed;HDOP;Satellites;BatteryLevel;Left;Right;Confirmed;Marked;Invalid;InsidePrivacyArea;Factor;Measurements;Tms1;Lus1;Rus1;Tms2;Lus2;Rus2;Tms3;Lus3;Rus3;Tms4;Lus4;Rus4;Tms5;Lus5;Rus5;Tms6;Lus6;Rus6;Tms7;Lus7;Rus7;Tms8;Lus8;Rus8;Tms9;Lus9;Rus9;Tms10;Lus10;Rus10;Tms11;Lus11;Rus11;Tms12;Lus12;Rus12;Tms13;Lus13;Rus13;Tms14;Lus14;Rus14;Tms15;Lus15;Rus15;Tms16;Lus16;Rus16;Tms17;Lus17;Rus17;Tms18;Lus18;Rus18;Tms19;Lus19;Rus19;Tms20;Lus20;Rus20;Tms21;Lus21;Rus21;Tms22;Lus22;Rus22;Tms23;Lus23;Rus23;Tms24;Lus24;Rus24;Tms25;Lus25;Rus25;Tms26;Lus26;Rus26;Tms27;Lus27;Rus27;Tms28;Lus28;Rus28;Tms29;Lus29;Rus29;Tms30;Lus30;Rus30;Tms31;Lus31;Rus31;Tms32;Lus32;Rus32;Tms33;Lus33;Rus33;Tms34;Lus34;Rus34;Tms35;Lus35;Rus35;Tms36;Lus36;Rus36;Tms37;Lus37;Rus37;Tms38;Lus38;Rus38;Tms39;Lus39;Rus39;Tms40;Lus40;Rus40;Tms41;Lus41;Rus41;Tms42;Lus42;Rus42;Tms43;Lus43;Rus43;Tms44;Lus44;Rus44;Tms45;Lus45;Rus45;Tms46;Lus46;Rus46;Tms47;Lus47;Rus47;Tms48;Lus48;Rus48;Tms49;Lus49;Rus49;Tms50;Lus50;Rus50;Tms51;Lus51;Rus51;Tms52;Lus52;Rus52;Tms53;Lus53;Rus53;Tms54;Lus54;Rus54;Tms55;Lus55;Rus55;Tms56;Lus56;Rus56;Tms57;Lus57;Rus57;Tms58;Lus58;Rus58;Tms59;Lus59;Rus59;Tms60;Lus60;Rus60;
|
|
||||||
21.11.2020;14:27:00;66890;;;;;;;3.83;4;3.99;;286;0;;0;0;58;5;0;;;41;;18355;67;;;87;;18374;113;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
`;
|
|
||||||
|
|
||||||
module.exports = { test1, test2, test3 };
|
|
|
@ -1,312 +0,0 @@
|
||||||
const csvParse = require('csv-parse/lib/sync');
|
|
||||||
const csvStringify = require('csv-stringify/lib/sync');
|
|
||||||
|
|
||||||
function _parseFloat(token) {
|
|
||||||
if (typeof token !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
token = token.trim();
|
|
||||||
|
|
||||||
if (token === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^nan$/i.test(token)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let f = parseFloat(token);
|
|
||||||
|
|
||||||
if (isNaN(f)) {
|
|
||||||
f = parseFloat(token.substring(0, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(f)) {
|
|
||||||
f = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _parseInt(token) {
|
|
||||||
const asFloat = _parseFloat(token);
|
|
||||||
|
|
||||||
if (asFloat !== null) {
|
|
||||||
return Math.floor(asFloat);
|
|
||||||
} else {
|
|
||||||
return asFloat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _parseString(token) {
|
|
||||||
if (typeof token !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// This time we do not trim -- because we assume that the quoting mechanism
|
|
||||||
// from CSV might have kicked in and we actually want the spacing around the
|
|
||||||
// token.
|
|
||||||
|
|
||||||
if (token === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceDollarNewlinesHack(body) {
|
|
||||||
// see if we are using the hack with $ as newlines, replace them for the csv parser
|
|
||||||
if (body.endsWith('$') || /insidePrivacyArea;\$/.test(body)) {
|
|
||||||
return body.replace(/\$/g, '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* parseTrackPoints(body, format = null) {
|
|
||||||
if (body instanceof Buffer) {
|
|
||||||
body = body.toString('utf-8')
|
|
||||||
}
|
|
||||||
body = replaceDollarNewlinesHack(body);
|
|
||||||
|
|
||||||
const detectedFormat = format != null ? format : detectFormat(body);
|
|
||||||
|
|
||||||
let parser;
|
|
||||||
switch (detectedFormat) {
|
|
||||||
case 'invalid':
|
|
||||||
throw new Error('track format cannot be detected');
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
parser = parseObsver1;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
parser = parseObsver2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* parser(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectFormat(body) {
|
|
||||||
body = replaceDollarNewlinesHack(body);
|
|
||||||
|
|
||||||
if (!body.length) {
|
|
||||||
return 'invalid';
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLinebreakIndex = body.indexOf('\n');
|
|
||||||
|
|
||||||
if (firstLinebreakIndex === -1) {
|
|
||||||
// We need at least one linebreak in the whole file, to separate header and
|
|
||||||
// data. If the file contains no header, it is in valid.
|
|
||||||
return 'invalid';
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstLine = body.substring(0, firstLinebreakIndex);
|
|
||||||
|
|
||||||
const match = firstLine.match(/(^|&)OBSDataFormat=([\d]+)($|&)/);
|
|
||||||
if (match) {
|
|
||||||
return Number(match[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have no metadata line, but start immediately with a header, AND it contains
|
|
||||||
// `;Rus`, it is a version 2
|
|
||||||
if (/^Date;Time.*;Rus/.test(firstLine)) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have no metadata line, but start immediately with a header, it is
|
|
||||||
// format version 1.
|
|
||||||
if (/^Date;Time/.test(firstLine)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we immediately start with data (a date, formatted as DD.MM.YYYY), then
|
|
||||||
// we have an old OBS not sending the header. It must therefore be old
|
|
||||||
// format, too.
|
|
||||||
if (/^[0-9]{2}\.[0-9]{2}\.[0-9]{4};/.test(firstLine)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'invalid';
|
|
||||||
}
|
|
||||||
|
|
||||||
function* parseObsver1(body) {
|
|
||||||
for (const record of csvParse(body, {
|
|
||||||
delimiter: ';',
|
|
||||||
encoding: 'utf8',
|
|
||||||
// We specify different column names here, as the order of columns was
|
|
||||||
// always the same, but their naming was different. By enforicing these
|
|
||||||
// column names we don't have to translate between them. Then we just
|
|
||||||
// ignore the first line (or any line that starts with "Date;").
|
|
||||||
// Original header usually is:
|
|
||||||
// Date;Time;Latitude;Longitude;Course;Speed;Right;Left;Confirmed;insidePrivacyArea
|
|
||||||
columns: ['date', 'time', 'latitude', 'longitude', 'course', 'speed', 'd1', 'd2', 'flag', 'private'],
|
|
||||||
relax_column_count: true,
|
|
||||||
cast(value, { column }) {
|
|
||||||
if (['latitude', 'longitude', 'course', 'speed'].includes(column)) {
|
|
||||||
return _parseFloat(value);
|
|
||||||
} else if (['d1', 'd2', 'flag'].includes(column)) {
|
|
||||||
return _parseInt(value);
|
|
||||||
} else if (column === 'private') {
|
|
||||||
return Boolean(_parseInt(value));
|
|
||||||
} else {
|
|
||||||
return _parseString(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})) {
|
|
||||||
if (record.date === 'Date') {
|
|
||||||
// ignore header line
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!record.latitude && !record.longitude) {
|
|
||||||
// invalid record, make sure lat/lng say `null` instead of `0`
|
|
||||||
record.latitude = null;
|
|
||||||
record.longitude = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// in old format, 255 or 999 means "no measurement"
|
|
||||||
if (record.d1 === 255 || record.d1 === 999) {
|
|
||||||
record.d1 = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.d2 === 255 || record.d2 === 999) {
|
|
||||||
record.d2 = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield record;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* parseObsver2(body) {
|
|
||||||
for (const record of csvParse(body, {
|
|
||||||
from_line: 2,
|
|
||||||
trim: true,
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
delimiter: ';',
|
|
||||||
encoding: 'utf8',
|
|
||||||
relax_column_count: true,
|
|
||||||
cast(value, context) {
|
|
||||||
if (value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let type;
|
|
||||||
switch (context.column) {
|
|
||||||
case 'Millis':
|
|
||||||
case 'Left':
|
|
||||||
case 'Right':
|
|
||||||
case 'Confirmed':
|
|
||||||
case 'Invalid':
|
|
||||||
case 'InsidePrivacyArea':
|
|
||||||
case 'Measurements':
|
|
||||||
case 'Satellites':
|
|
||||||
type = 'int';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Date':
|
|
||||||
case 'Time':
|
|
||||||
case 'Comment':
|
|
||||||
case 'Marked':
|
|
||||||
type = 'string';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Latitude':
|
|
||||||
case 'Longitude':
|
|
||||||
case 'Altitude':
|
|
||||||
case 'Course':
|
|
||||||
case 'Speed':
|
|
||||||
case 'HDOP':
|
|
||||||
case 'BatteryLevel':
|
|
||||||
case 'Factor':
|
|
||||||
type = 'float';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
type = /^(Tms|Lus|Rus)/.test(context.column) ? 'int' : 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'int':
|
|
||||||
return _parseInt(value);
|
|
||||||
|
|
||||||
case 'float':
|
|
||||||
return _parseFloat(value);
|
|
||||||
|
|
||||||
case 'string':
|
|
||||||
return _parseString(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})) {
|
|
||||||
// We convert the new format back to the old format for storage here, until
|
|
||||||
// we upgrade the storage format as well to include all data. But we'll
|
|
||||||
// have to upgrade the obsApp first.
|
|
||||||
yield {
|
|
||||||
date: record.Date,
|
|
||||||
time: record.Time,
|
|
||||||
latitude: record.Latitude,
|
|
||||||
longitude: record.Longitude,
|
|
||||||
course: record.Course,
|
|
||||||
speed: record.Speed,
|
|
||||||
d1: record.Left,
|
|
||||||
d2: record.Right,
|
|
||||||
flag: Boolean(record.Confirmed),
|
|
||||||
private: Boolean(record.InsidePrivacyArea),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function normalizes a User-Agent header for storage in the database. It
|
|
||||||
* make sure that we only store the user-agent if it matches the pattern
|
|
||||||
* `OBS/*`, and extracts that part of the user agent, if it contains more
|
|
||||||
* information. This is the only part we are interested in, the
|
|
||||||
* remainder is too privacy sensitive to keep.
|
|
||||||
*/
|
|
||||||
function normalizeUserAgent(userAgent) {
|
|
||||||
if (!userAgent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = userAgent.match(/\bOBS\/[^\s]+/);
|
|
||||||
if (match) {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildObsver1(points) {
|
|
||||||
return csvStringify(points, {
|
|
||||||
columns: [
|
|
||||||
{ key: 'date', header: 'Date' },
|
|
||||||
{ key: 'time', header: 'Time' },
|
|
||||||
{ key: 'latitude', header: 'Latitude' },
|
|
||||||
{ key: 'longitude', header: 'Longitude' },
|
|
||||||
{ key: 'course', header: 'Course' },
|
|
||||||
{ key: 'speed', header: 'Speed' },
|
|
||||||
{ key: 'd1', header: 'Right' },
|
|
||||||
{ key: 'd2', header: 'Left' },
|
|
||||||
{ key: 'flag', header: 'Confirmed' },
|
|
||||||
{ key: 'private', header: 'insidePrivacyArea' },
|
|
||||||
],
|
|
||||||
cast: {
|
|
||||||
boolean: (v) => (v ? '1' : '0'),
|
|
||||||
},
|
|
||||||
delimiter: ';',
|
|
||||||
header: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
detectFormat,
|
|
||||||
normalizeUserAgent,
|
|
||||||
parseObsver1,
|
|
||||||
parseObsver2,
|
|
||||||
parseTrackPoints,
|
|
||||||
replaceDollarNewlinesHack,
|
|
||||||
buildObsver1,
|
|
||||||
};
|
|
|
@ -1,171 +0,0 @@
|
||||||
const {
|
|
||||||
buildObsver1,
|
|
||||||
detectFormat,
|
|
||||||
normalizeUserAgent,
|
|
||||||
parseObsver1,
|
|
||||||
parseObsver2,
|
|
||||||
parseTrackPoints,
|
|
||||||
replaceDollarNewlinesHack,
|
|
||||||
} = require('./tracks');
|
|
||||||
|
|
||||||
const { test1, test2, test3 } = require('./_tracks_testdata');
|
|
||||||
|
|
||||||
describe('parseTrackPoints', () => {
|
|
||||||
it('is a function', () => {
|
|
||||||
expect(typeof parseTrackPoints).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('works on the sample data with an empty track', () => {
|
|
||||||
const points = Array.from(parseTrackPoints(test1));
|
|
||||||
expect(points).toHaveLength(324);
|
|
||||||
expect(points[0]).toEqual({
|
|
||||||
date: '12.07.2020',
|
|
||||||
time: '09:02:59',
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
course: 0,
|
|
||||||
speed: 0,
|
|
||||||
d1: null,
|
|
||||||
d2: null,
|
|
||||||
flag: 0,
|
|
||||||
private: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseObsver1', () => {
|
|
||||||
it('can parse sample data', () => {
|
|
||||||
const points = Array.from(parseObsver1(replaceDollarNewlinesHack(test1)));
|
|
||||||
expect(points).toHaveLength(324);
|
|
||||||
expect(points[0]).toEqual({
|
|
||||||
date: '12.07.2020',
|
|
||||||
time: '09:02:59',
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
course: 0,
|
|
||||||
speed: 0,
|
|
||||||
d1: null,
|
|
||||||
d2: null,
|
|
||||||
flag: 0,
|
|
||||||
private: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseObsver2', () => {
|
|
||||||
it('can parse sample data', () => {
|
|
||||||
const points = Array.from(parseObsver2(test2));
|
|
||||||
expect(points).toHaveLength(18);
|
|
||||||
expect(points[0]).toEqual({
|
|
||||||
date: '18.11.2020',
|
|
||||||
time: '16:05:59',
|
|
||||||
latitude: 48.723224,
|
|
||||||
longitude: 9.094103,
|
|
||||||
course: 189.86,
|
|
||||||
speed: 3.2,
|
|
||||||
d1: 770,
|
|
||||||
d2: null,
|
|
||||||
flag: false,
|
|
||||||
private: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// this is a non-private, flagged point (i.e. "Confirmed" overtaking)
|
|
||||||
expect(points[17]).toEqual({
|
|
||||||
date: '18.11.2020',
|
|
||||||
time: '16:06:16',
|
|
||||||
latitude: 48.723109,
|
|
||||||
longitude: 9.093963,
|
|
||||||
course: 247.62,
|
|
||||||
speed: 0,
|
|
||||||
d1: 5,
|
|
||||||
d2: 89,
|
|
||||||
flag: true,
|
|
||||||
private: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('detectFormat', () => {
|
|
||||||
it('detects format 1', () => {
|
|
||||||
expect(detectFormat(test1)).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detects format 2', () => {
|
|
||||||
expect(detectFormat(test2)).toBe(2);
|
|
||||||
expect(detectFormat(test3)).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detects invalid format', () => {
|
|
||||||
expect(detectFormat('foobar\nbaz')).toBe('invalid');
|
|
||||||
expect(detectFormat('')).toBe('invalid');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('normalizeUserAgent', () => {
|
|
||||||
it('is a function', () => {
|
|
||||||
expect(typeof normalizeUserAgent).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores falsy values', () => {
|
|
||||||
expect(normalizeUserAgent(null)).toBe(null);
|
|
||||||
expect(normalizeUserAgent('')).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores normal browser agents', () => {
|
|
||||||
const browserAgents = [
|
|
||||||
'Mozilla/5.0 (Linux; Android 6.0.1; Nexus 6P Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.83 Mobile Safari/537.36',
|
|
||||||
'Mozilla/5.0 (Linux; Android 6.0; HTC One M9 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.3',
|
|
||||||
'Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
|
|
||||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A5370a Safari/604.1',
|
|
||||||
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const browserAgent of browserAgents) {
|
|
||||||
expect(normalizeUserAgent(browserAgent)).toBe(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('detects OBS versions', () => {
|
|
||||||
const agents = ['OBS/123', 'OBS/2', 'OBS/1.2.3.4.5-rc123'];
|
|
||||||
|
|
||||||
for (const agent of agents) {
|
|
||||||
expect(normalizeUserAgent(agent)).toBe(agent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('extracts OBS versions from extended formats', () => {
|
|
||||||
const agents = ['foo OBS/123', 'OBS/123 bar', 'foo OBS/123 bar'];
|
|
||||||
|
|
||||||
for (const agent of agents) {
|
|
||||||
expect(normalizeUserAgent(agent)).toBe('OBS/123');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('buildObsver1', () => {
|
|
||||||
it('is a function', () => {
|
|
||||||
expect(typeof normalizeUserAgent).toBe('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('transforms properly back and forth', () => {
|
|
||||||
const inputString = replaceDollarNewlinesHack(test1);
|
|
||||||
|
|
||||||
const points1 = Array.from(parseObsver1(inputString));
|
|
||||||
const builtString = buildObsver1(points1);
|
|
||||||
const points2 = Array.from(parseObsver1(builtString));
|
|
||||||
|
|
||||||
expect(points2).toEqual(points1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('produces a header', () => {
|
|
||||||
const builtString = buildObsver1([]);
|
|
||||||
expect(builtString).toBe('Date;Time;Latitude;Longitude;Course;Speed;Right;Left;Confirmed;insidePrivacyArea\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('produces empty rows', () => {
|
|
||||||
const builtString = buildObsver1([{}]);
|
|
||||||
expect(builtString).toBe(
|
|
||||||
'Date;Time;Latitude;Longitude;Course;Speed;Right;Left;Confirmed;insidePrivacyArea\n;;;;;;;;;\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,49 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const uniqueValidator = require('mongoose-unique-validator');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
token: { index: true, type: String, required: true, unique: true },
|
|
||||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
|
||||||
clientId: { type: String, required: true },
|
|
||||||
expiresAt: { type: Date, required: true },
|
|
||||||
scope: { type: String, required: true, defaultValue: '*' },
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
schema.plugin(uniqueValidator, { message: 'reused token' });
|
|
||||||
|
|
||||||
class AccessTokenClass extends mongoose.Model {
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
token: this.token,
|
|
||||||
expires: this.expires,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid() {
|
|
||||||
return this.expiresAt < new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
toHeaderString() {
|
|
||||||
return 'Bearer ' + this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
static generate(options, expiresInSeconds = 24 * 60 * 60) {
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
return new AccessToken({
|
|
||||||
...options,
|
|
||||||
token,
|
|
||||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(AccessTokenClass);
|
|
||||||
|
|
||||||
const AccessToken = mongoose.model('AccessToken', schema);
|
|
||||||
|
|
||||||
module.exports = AccessToken;
|
|
|
@ -1,32 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
code: { type: String, unique: true, required: true },
|
|
||||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
|
||||||
clientId: { type: String, required: true },
|
|
||||||
scope: { type: String, required: true, defaultValue: '*' },
|
|
||||||
redirectUri: { type: String, required: true },
|
|
||||||
expiresAt: { type: Date, required: true },
|
|
||||||
codeChallenge: { type: String, required: true }, // no need to store the method, it is always "S256"
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
class AuthorizationCodeClass extends mongoose.Model {
|
|
||||||
static generate(options, expiresInSeconds = 60) {
|
|
||||||
const code = crypto.randomBytes(8).toString('hex');
|
|
||||||
|
|
||||||
return new AuthorizationCode({
|
|
||||||
...options,
|
|
||||||
code,
|
|
||||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(AuthorizationCodeClass)
|
|
||||||
|
|
||||||
const AuthorizationCode = mongoose.model('AuthorizationCode', schema);
|
|
||||||
module.exports = AuthorizationCode
|
|
|
@ -1,24 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
body: String,
|
|
||||||
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
|
||||||
track: { type: mongoose.Schema.Types.ObjectId, ref: 'Track' },
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
class CommentClass extends mongoose.Model {
|
|
||||||
toJSONFor(user) {
|
|
||||||
return {
|
|
||||||
id: this._id,
|
|
||||||
body: this.body,
|
|
||||||
createdAt: this.createdAt,
|
|
||||||
author: this.author.toProfileJSONFor(user),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(CommentClass)
|
|
||||||
module.exports = mongoose.model('Comment', schema);
|
|
|
@ -1,46 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const uniqueValidator = require('mongoose-unique-validator');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
const AccessToken = require('./AccessToken')
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
token: { index: true, type: String, required: true, unique: true },
|
|
||||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
|
||||||
clientId: { type: String, required: true },
|
|
||||||
expiresAt: { type: Date, required: false },
|
|
||||||
scope: { type: String, required: true, defaultValue: '*' },
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
schema.plugin(uniqueValidator, { message: 'reused token' });
|
|
||||||
|
|
||||||
class RefreshTokenClass extends mongoose.Model {
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
token: this.token,
|
|
||||||
expires: this.expires,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid() {
|
|
||||||
return this.expiresAt == null || this.expiresAt < new Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
static generate(options, expiresInSeconds = 24 * 60 * 60) {
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
return new RefreshToken({
|
|
||||||
...options,
|
|
||||||
token,
|
|
||||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(RefreshTokenClass);
|
|
||||||
|
|
||||||
const RefreshToken = mongoose.model('RefreshToken', schema);
|
|
||||||
module.exports = RefreshToken
|
|
|
@ -1,306 +0,0 @@
|
||||||
const crypto = require('crypto');
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const uniqueValidator = require('mongoose-unique-validator');
|
|
||||||
const { DateTime } = require('luxon');
|
|
||||||
const slug = require('slug');
|
|
||||||
const path = require('path');
|
|
||||||
const sanitize = require('sanitize-filename');
|
|
||||||
const fs = require('fs');
|
|
||||||
const uuid = require('uuid/v4');
|
|
||||||
|
|
||||||
const { TRACKS_DIR } = require('../paths');
|
|
||||||
const queue = require('../queue');
|
|
||||||
|
|
||||||
const statisticsSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
recordedAt: Date,
|
|
||||||
recordedUntil: Date,
|
|
||||||
duration: Number,
|
|
||||||
length: Number,
|
|
||||||
segments: Number,
|
|
||||||
numEvents: Number,
|
|
||||||
numMeasurements: Number,
|
|
||||||
numValid: Number,
|
|
||||||
},
|
|
||||||
{ timestamps: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
// A (partially or entirely random generated) string that can be used as a
|
|
||||||
// public identifier
|
|
||||||
slug: { type: String, lowercase: true, unique: true },
|
|
||||||
|
|
||||||
// The title for this track.
|
|
||||||
title: String,
|
|
||||||
|
|
||||||
// The status of this track, whether it is to be processed, is currently
|
|
||||||
// being processed, or has completed or errored.
|
|
||||||
processingStatus: {
|
|
||||||
type: String,
|
|
||||||
enum: ['pending', 'processing', 'complete', 'error'],
|
|
||||||
default: 'pending',
|
|
||||||
},
|
|
||||||
processingJobId: String,
|
|
||||||
|
|
||||||
// Output from the proccessing routines regarding this track. Might be
|
|
||||||
// displayed to the owner or administrators to help in debugging. Should be
|
|
||||||
// set to `null` if no processing has not been finished.
|
|
||||||
processingLog: String,
|
|
||||||
|
|
||||||
// Set to true if the user customized the title. Disables auto-generating
|
|
||||||
// an updated title when the track is (re-)processed.
|
|
||||||
customizedTitle: { type: Boolean, default: false },
|
|
||||||
|
|
||||||
// A user-provided description of the track. May contain markdown.
|
|
||||||
description: String,
|
|
||||||
|
|
||||||
// Whether this track is visible (anonymized) in the public track list or not.
|
|
||||||
public: { type: Boolean, default: false },
|
|
||||||
|
|
||||||
// Whether this track should be exported to the public track database
|
|
||||||
// (after anonymization).
|
|
||||||
includeInPublicDatabase: { type: Boolean, default: false },
|
|
||||||
|
|
||||||
// The user agent string, or a part thereof, that was used to upload this
|
|
||||||
// track. Usually contains only the OBS version, other user agents are
|
|
||||||
// discarded due to being irrelevant.
|
|
||||||
uploadedByUserAgent: String,
|
|
||||||
|
|
||||||
// The name of the original file, as provided during upload. Used for
|
|
||||||
// providing a download with the same name, and for display in the
|
|
||||||
// frontend.
|
|
||||||
originalFileName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
validate: {
|
|
||||||
validator: function (v) {
|
|
||||||
// Must be a sane filename, i.e. not change when being sanitized
|
|
||||||
return sanitize(v) === v && v.length > 0 && /.+\.csv$/i.test(v);
|
|
||||||
},
|
|
||||||
message: (props) => `${props.value} is not a valid filename`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// A hash of the original file's contents. Nobody can upload the same track twice.
|
|
||||||
originalFileHash: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Where the files are stored, relative to a group directory like
|
|
||||||
// TRACKS_DIR or PROCESSING_DIR.
|
|
||||||
filePath: String,
|
|
||||||
|
|
||||||
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment' }],
|
|
||||||
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
|
||||||
|
|
||||||
statistics: statisticsSchema,
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
schema.index({ author: 1, originalFileHash: 1 }, { unique: true });
|
|
||||||
|
|
||||||
schema.plugin(uniqueValidator, { message: 'is already taken' });
|
|
||||||
|
|
||||||
schema.pre('validate', async function (next) {
|
|
||||||
try {
|
|
||||||
if (!this.slug) {
|
|
||||||
this.slugify();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.filePath) {
|
|
||||||
await this.generateFilePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 0..4 Night, 4..10 Morning, 10..14 Noon, 14..18 Afternoon, 18..22 Evening, 22..00 Night
|
|
||||||
// Two hour intervals
|
|
||||||
const DAYTIMES = [
|
|
||||||
'Night', // 0h - 2h
|
|
||||||
'Night', // 2h - 4h
|
|
||||||
'Morning', // 4h - 6h
|
|
||||||
'Morning', // 6h - 8h
|
|
||||||
'Morning', // 8h - 10h
|
|
||||||
'Noon', // 10h - 12h
|
|
||||||
'Noon', // 12h - 14h
|
|
||||||
'Afternoon', // 14h - 16h
|
|
||||||
'Afternoon', // 16h - 18h
|
|
||||||
'Evening', // 18h - 20h
|
|
||||||
'Evening', // 20h - 22h
|
|
||||||
'Night', // 22h - 24h
|
|
||||||
];
|
|
||||||
|
|
||||||
function getDaytime(dateTime) {
|
|
||||||
return DAYTIMES[Math.floor((dateTime.hour % 24) / 2)];
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrackClass extends mongoose.Model {
|
|
||||||
slugify() {
|
|
||||||
this.slug = slug(this.title || 'track') + '-' + ((Math.random() * Math.pow(36, 6)) | 0).toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateFilePath() {
|
|
||||||
await this.populate('author');
|
|
||||||
this.filePath = path.join(this.author.username, this.slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisibleTo(user) {
|
|
||||||
if (this.public) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user._id.equals(this.author._id)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isVisibleToPrivate(user) {
|
|
||||||
return user && user._id.equals(this.author._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _ensureDirectoryExists() {
|
|
||||||
if (!this.filePath) {
|
|
||||||
await this.generateFilePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = path.dirname(this.getOriginalFilePath());
|
|
||||||
await fs.promises.mkdir(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
getOriginalFilePath() {
|
|
||||||
if (!this.filePath) {
|
|
||||||
throw new Error('Cannot get original file path, `filePath` is not yet set. Call `generateFilePath()` first.');
|
|
||||||
}
|
|
||||||
return path.join(TRACKS_DIR, this.filePath, 'original.csv');
|
|
||||||
}
|
|
||||||
|
|
||||||
async writeToOriginalFile(fileBody) {
|
|
||||||
await this._ensureDirectoryExists();
|
|
||||||
await fs.promises.writeFile(this.getOriginalFilePath(), fileBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateFileBodyUniqueness(fileBody) {
|
|
||||||
// Generate hash
|
|
||||||
const hash = crypto.createHash('sha512').update(fileBody).digest('hex');
|
|
||||||
|
|
||||||
const existingTracks = await Track.find({ originalFileHash: hash, author: this.author });
|
|
||||||
if (existingTracks.length === 0 || (existingTracks.length === 1 && existingTracks[0]._id.equals(this._id))) {
|
|
||||||
this.originalFileHash = hash;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Track file already uploaded.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks this track as needing processing.
|
|
||||||
*
|
|
||||||
* Also deletes all stored information that is derived during processing from
|
|
||||||
* the database, such that it may be filled again with correct information
|
|
||||||
* during the processing operation.
|
|
||||||
*
|
|
||||||
* Saves the track as well, so it is up to date when the worker receives it.
|
|
||||||
*/
|
|
||||||
async queueProcessing() {
|
|
||||||
this.processingStatus = 'pending';
|
|
||||||
this.processingLog = null;
|
|
||||||
this.processingJobId = uuid();
|
|
||||||
this.statistics = null;
|
|
||||||
|
|
||||||
await this.save();
|
|
||||||
|
|
||||||
await queue.add(
|
|
||||||
'processTrack',
|
|
||||||
{
|
|
||||||
trackId: this._id.toString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
jobId: this.processingJobId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async readProcessingResults(success = true) {
|
|
||||||
// Copies some information into this object from the outputs of the
|
|
||||||
// processing step. This allows general statistics to be formed, and other
|
|
||||||
// information to be displayed, without having to read individual files
|
|
||||||
// from disk. Each field set here should be unsed in `queueProcessing`.
|
|
||||||
// This routine also moves the `processingStatus` along.
|
|
||||||
}
|
|
||||||
|
|
||||||
async autoGenerateTitle() {
|
|
||||||
if (this.customizedTitle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to figure out when this file was recorded. Either we have it in then
|
|
||||||
// statistics, e.g. after parsing and processing the track, or we can maybe
|
|
||||||
// derive it from the filename.
|
|
||||||
let recordedAt = null;
|
|
||||||
|
|
||||||
if (this.statistics && this.statistics.recordedAt != null) {
|
|
||||||
recordedAt = DateTime.fromJSDate(this.statistics.recordedAt);
|
|
||||||
} else if (this.originalFileName) {
|
|
||||||
const match = this.originalFileName.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}\.[0-9]{2}\.[0-9]{2}/);
|
|
||||||
if (match) {
|
|
||||||
recordedAt = DateTime.fromFormat(match[0], "yyyy-MM-dd'T'HH.mm.ss");
|
|
||||||
if (!recordedAt.isValid) {
|
|
||||||
recordedAt = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recordedAt) {
|
|
||||||
const daytime = getDaytime(recordedAt);
|
|
||||||
this.title = `${daytime} ride on ${recordedAt.toLocaleString(recordedAt.DATE_MED)}`;
|
|
||||||
await this.save();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detecting recording date failed, use filename
|
|
||||||
if (this.originalFileName) {
|
|
||||||
this.title = _.upperFirst(_.words(this.originalFileName.replace(/(\.obsdata)?\.csv$/, '')).join(' '));
|
|
||||||
await this.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSONFor(user) {
|
|
||||||
const includePrivateFields = user && user._id.equals(this.author._id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
slug: this.slug,
|
|
||||||
title: this.title,
|
|
||||||
description: this.description,
|
|
||||||
createdAt: this.createdAt,
|
|
||||||
updatedAt: this.updatedAt,
|
|
||||||
public: this.public,
|
|
||||||
author: this.author.toProfileJSONFor(user),
|
|
||||||
statistics: this.statistics,
|
|
||||||
processingStatus: this.processingStatus,
|
|
||||||
...(includePrivateFields
|
|
||||||
? {
|
|
||||||
uploadedByUserAgent: this.uploadedByUserAgent,
|
|
||||||
originalFileName: this.originalFileName,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(TrackClass);
|
|
||||||
const Track = mongoose.model('Track', schema);
|
|
||||||
module.exports = Track;
|
|
|
@ -1,91 +0,0 @@
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const uniqueValidator = require('mongoose-unique-validator');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const config = require('../config')
|
|
||||||
|
|
||||||
const schema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
username: {
|
|
||||||
type: String,
|
|
||||||
lowercase: true,
|
|
||||||
unique: true,
|
|
||||||
required: [true, "can't be blank"],
|
|
||||||
match: [/^[a-zA-Z0-9]+$/, 'is invalid'],
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
lowercase: true,
|
|
||||||
unique: true,
|
|
||||||
required: [true, "can't be blank"],
|
|
||||||
match: [/\S+@\S+\.\S+/, 'is invalid'],
|
|
||||||
index: true,
|
|
||||||
},
|
|
||||||
bio: String,
|
|
||||||
image: String,
|
|
||||||
areTracksVisibleForAll: Boolean,
|
|
||||||
hash: String,
|
|
||||||
salt: String,
|
|
||||||
needsEmailValidation: Boolean,
|
|
||||||
verificationToken: String,
|
|
||||||
resetToken: {
|
|
||||||
token: String,
|
|
||||||
expires: Date,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ timestamps: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
schema.plugin(uniqueValidator, { message: 'ist bereits vergeben. Sorry!' });
|
|
||||||
|
|
||||||
class UserClass extends mongoose.Model {
|
|
||||||
validPassword(password) {
|
|
||||||
const hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
|
||||||
return this.hash === hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPassword(password) {
|
|
||||||
this.salt = crypto.randomBytes(16).toString('hex');
|
|
||||||
this.hash = crypto.pbkdf2Sync(password, this.salt, 10000, 512, 'sha512').toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
generateJWT() {
|
|
||||||
const today = new Date();
|
|
||||||
const exp = new Date(today);
|
|
||||||
exp.setDate(today.getDate() + 60);
|
|
||||||
|
|
||||||
return jwt.sign(
|
|
||||||
{
|
|
||||||
id: this._id,
|
|
||||||
username: this.username,
|
|
||||||
exp: parseInt(exp.getTime() / 1000),
|
|
||||||
},
|
|
||||||
config.jwtSecret,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
toAuthJSON() {
|
|
||||||
return {
|
|
||||||
username: this.username,
|
|
||||||
email: this.email,
|
|
||||||
token: this.generateJWT(),
|
|
||||||
bio: this.bio,
|
|
||||||
image: this.image,
|
|
||||||
areTracksVisibleForAll: this.areTracksVisibleForAll,
|
|
||||||
apiKey: this._id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toProfileJSONFor(user) {
|
|
||||||
return {
|
|
||||||
username: this.username,
|
|
||||||
bio: this.bio,
|
|
||||||
image: this.image,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.loadClass(UserClass);
|
|
||||||
const User = mongoose.model('User', schema);
|
|
||||||
module.exports = User
|
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports.AccessToken = require('./AccessToken')
|
|
||||||
module.exports.AuthorizationCode = require('./AuthorizationCode')
|
|
||||||
module.exports.Comment = require('./Comment')
|
|
||||||
module.exports.RefreshToken = require('./RefreshToken')
|
|
||||||
module.exports.Track = require('./Track')
|
|
||||||
module.exports.User = require('./User')
|
|
|
@ -1,215 +0,0 @@
|
||||||
const passport = require('passport');
|
|
||||||
const { Strategy: LocalStrategy } = require('passport-local');
|
|
||||||
const { Strategy: BearerStrategy } = require('passport-http-bearer');
|
|
||||||
const { Strategy: JwtStrategy } = require('passport-jwt');
|
|
||||||
const { Strategy: CustomStrategy } = require('passport-custom');
|
|
||||||
|
|
||||||
const { User, AccessToken, RefreshToken } = require('./models');
|
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
// used to serialize the user for the session
|
|
||||||
passport.serializeUser(function (user, done) {
|
|
||||||
done(null, user._id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// used to deserialize the user
|
|
||||||
passport.deserializeUser(function (id, done) {
|
|
||||||
User.findById(id, function (err, user) {
|
|
||||||
done(err, user);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loginWithPassword(email, password, done) {
|
|
||||||
try {
|
|
||||||
const user = await User.findOne({ email: email });
|
|
||||||
if (!user || !user.validPassword(password)) {
|
|
||||||
return done(new Error('invalid credentials'), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regardless of whether login is required, if you're logged in as an
|
|
||||||
// unverified user, produce an error.
|
|
||||||
if (user.needsEmailValidation) {
|
|
||||||
return done(new Error('email not verified'), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return done(null, user);
|
|
||||||
} catch (err) {
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'usernameAndPassword',
|
|
||||||
new LocalStrategy(
|
|
||||||
{
|
|
||||||
usernameField: 'user[email]',
|
|
||||||
passwordField: 'user[password]',
|
|
||||||
session: false,
|
|
||||||
},
|
|
||||||
loginWithPassword,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'usernameAndPasswordSession',
|
|
||||||
new LocalStrategy(
|
|
||||||
{
|
|
||||||
usernameField: 'email',
|
|
||||||
passwordField: 'password',
|
|
||||||
session: true,
|
|
||||||
},
|
|
||||||
loginWithPassword,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
function getRequestToken(req, tokenTypes = ['Token', 'Bearer']) {
|
|
||||||
const authorization = req.headers.authorization;
|
|
||||||
if (typeof authorization !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tokenType, token] = authorization.split(' ');
|
|
||||||
|
|
||||||
if (tokenTypes.includes(tokenType)) {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'jwt',
|
|
||||||
new JwtStrategy(
|
|
||||||
{
|
|
||||||
secretOrKey: config.jwtSecret,
|
|
||||||
jwtFromRequest: getRequestToken,
|
|
||||||
algorithms: ['HS256'],
|
|
||||||
},
|
|
||||||
async function (token, done) {
|
|
||||||
try {
|
|
||||||
// we used to put the user ID into the token directly :(
|
|
||||||
const { id } = token;
|
|
||||||
const user = await User.findById(id);
|
|
||||||
return done(null, user || false);
|
|
||||||
} catch (err) {
|
|
||||||
return done(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'accessToken',
|
|
||||||
new BearerStrategy(async function (token, done) {
|
|
||||||
try {
|
|
||||||
const accessToken = await AccessToken.findOne({ token }).populate('user');
|
|
||||||
if (accessToken && accessToken.user) {
|
|
||||||
// TODO: scope
|
|
||||||
return done(null, accessToken.user, { scope: accessToken.scope });
|
|
||||||
} else {
|
|
||||||
return done(null, false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return done(err);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'refreshToken',
|
|
||||||
new BearerStrategy(async function (token, done) {
|
|
||||||
try {
|
|
||||||
const refreshToken = await RefreshToken.findOne({ token }).populate('user');
|
|
||||||
if (refreshToken && refreshToken.user) {
|
|
||||||
// TODO: scope
|
|
||||||
return done(null, refreshToken.user, { scope: 'auth.refresh' });
|
|
||||||
} else {
|
|
||||||
return done(null, false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return done(err);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
passport.use(
|
|
||||||
'userId',
|
|
||||||
new CustomStrategy(async (req, callback) => {
|
|
||||||
try {
|
|
||||||
let userId;
|
|
||||||
|
|
||||||
const headerToken = getRequestToken(req, ['OBSUserId']);
|
|
||||||
if (headerToken && headerToken.length === 24) {
|
|
||||||
userId = headerToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
const bodyId = req.body && req.body.id;
|
|
||||||
if (bodyId && bodyId.length === 24) {
|
|
||||||
userId = bodyId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let user;
|
|
||||||
if (userId) {
|
|
||||||
user = await User.findById(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(null, user || false);
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function creates a middleware that does a passport authentication.
|
|
||||||
*/
|
|
||||||
function createMiddleware(strategies, required = true, session = false) {
|
|
||||||
return (req, res, next) => {
|
|
||||||
passport.authenticate(strategies, { session }, (err, user, info) => {
|
|
||||||
// If this authentication produced an error, throw it. In a chain of
|
|
||||||
// multiple strategies, errors are ignored, unless every strategy errors.
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If you *must* be logged in for this action, require a user.
|
|
||||||
if (required && !user) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regardless of whether login is required, if you're logged in as an
|
|
||||||
// unverified user, produce an error.
|
|
||||||
if (user && user.needsEmailValidation) {
|
|
||||||
return res.status(403).json({ errors: { 'E-Mail-Bestätigung': 'noch nicht erfolgt' } });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = user;
|
|
||||||
req.scope = (info && info.scope) || '*';
|
|
||||||
|
|
||||||
return next();
|
|
||||||
})(req, res, next);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// these are the standard authentication mechanisms, for when you want user
|
|
||||||
// information in the route, and either require a login, or don't care
|
|
||||||
optional: createMiddleware(['jwt', 'accessToken'], false),
|
|
||||||
required: createMiddleware(['jwt', 'accessToken'], true),
|
|
||||||
|
|
||||||
// required to check username and passwort for generating a new token, e.g.
|
|
||||||
// on the /users/login route, and later on oauth routes
|
|
||||||
usernameAndPassword: createMiddleware('usernameAndPassword', true),
|
|
||||||
|
|
||||||
usernameAndPasswordSession: createMiddleware('usernameAndPasswordSession', false, true),
|
|
||||||
|
|
||||||
// will be used to verify a refresh token on the route that will exchange the
|
|
||||||
// refresh token for a new access token (not in use yet)
|
|
||||||
refreshToken: createMiddleware('refreshToken', true),
|
|
||||||
|
|
||||||
// for track upload, we still allow "userId" for a while
|
|
||||||
requiredWithUserId: createMiddleware(['jwt', 'accessToken', 'userId'], true),
|
|
||||||
};
|
|
|
@ -1,29 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const API_ROOT_DIR = path.resolve(__dirname, '../');
|
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, '../../data/');
|
|
||||||
|
|
||||||
// Contains the subtree for processing files
|
|
||||||
const PROCESSING_DIR = path.join(DATA_DIR, 'processing');
|
|
||||||
const PROCESSING_OUTPUT_DIR = path.join(DATA_DIR, 'processing-output');
|
|
||||||
|
|
||||||
// Contains the subtree for processing files, without privatization techniques,
|
|
||||||
// used only for display of tracks to authors
|
|
||||||
const PROCESSING_DIR_PRIVATE = path.join(DATA_DIR, 'private');
|
|
||||||
|
|
||||||
// Contains original track files
|
|
||||||
const TRACKS_DIR = path.join(DATA_DIR, 'tracks');
|
|
||||||
|
|
||||||
// Cache directory for all obs-face calls
|
|
||||||
const OBS_FACE_CACHE_DIR = path.join(DATA_DIR, 'obs-face-cache');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
API_ROOT_DIR,
|
|
||||||
DATA_DIR,
|
|
||||||
PROCESSING_DIR,
|
|
||||||
PROCESSING_OUTPUT_DIR,
|
|
||||||
PROCESSING_DIR_PRIVATE,
|
|
||||||
TRACKS_DIR,
|
|
||||||
OBS_FACE_CACHE_DIR,
|
|
||||||
};
|
|
|
@ -1,12 +0,0 @@
|
||||||
const Bull = require('bull');
|
|
||||||
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
module.exports = new Bull('processQueue', config.redisUrl, {
|
|
||||||
settings: {
|
|
||||||
// if the worker process is killed and restarted, e.g. due to reboot or
|
|
||||||
// upgrade, it is okay to wait for a timeout on the job and restart it
|
|
||||||
maxStalledCount: 3,
|
|
||||||
lockDuration: 120 * 1000,
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,25 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
|
|
||||||
router.use('/', require('./users'));
|
|
||||||
router.use('/profiles', require('./profiles'));
|
|
||||||
router.use('/tracks', require('./tracks'));
|
|
||||||
router.use('/tags', require('./tags'));
|
|
||||||
router.use('/accounts', require('../../accounts/accounts.controller'));
|
|
||||||
router.use('/stats', require('./stats'));
|
|
||||||
router.use('/info', require('./info'));
|
|
||||||
|
|
||||||
router.use(function (err, req, res, next) {
|
|
||||||
if (err.name === 'ValidationError') {
|
|
||||||
return res.status(422).json({
|
|
||||||
errors: Object.keys(err.errors).reduce(function (errors, key) {
|
|
||||||
errors[key] = err.errors[key].message;
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}, {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return next(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,11 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
|
|
||||||
const { version } = require('../../../package.json');
|
|
||||||
|
|
||||||
router.route('/').get((req, res) => {
|
|
||||||
res.json({
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,30 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
|
||||||
const auth = require('../../passport');
|
|
||||||
const { User } = require('../../models');
|
|
||||||
|
|
||||||
// Preload user profile on routes with ':username'
|
|
||||||
router.param('username', async function (req, res, next, username) {
|
|
||||||
try {
|
|
||||||
const user = await User.findOne({ username: username });
|
|
||||||
if (!user) {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.profile = user;
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/:username',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
return res.json({ profile: req.profile.toProfileJSONFor(req.user) });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,107 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
const { DateTime } = require('luxon');
|
|
||||||
|
|
||||||
const { Track, User } = require('../../models');
|
|
||||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
|
||||||
const auth = require('../../passport');
|
|
||||||
|
|
||||||
// round to this number of meters for privacy reasons
|
|
||||||
const TRACK_LENGTH_ROUNDING = 1000;
|
|
||||||
|
|
||||||
// round to this number of seconds for privacy reasons
|
|
||||||
const TRACK_DURATION_ROUNDING = 120;
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const start = DateTime.fromISO(req.query.start);
|
|
||||||
const end = DateTime.fromISO(req.query.end);
|
|
||||||
|
|
||||||
const dateFilter = {
|
|
||||||
$ne: null,
|
|
||||||
...(start.isValid ? { $gte: start.toJSDate() } : {}),
|
|
||||||
...(end.isValid ? { $lt: end.toJSDate() } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let userFilter;
|
|
||||||
if (req.query.user) {
|
|
||||||
const user = await User.findOne({ username: req.query.user });
|
|
||||||
|
|
||||||
// Only the user can look for their own stats, for now
|
|
||||||
if (req.user && req.user._id.equals(user._id)) {
|
|
||||||
userFilter = user._id;
|
|
||||||
} else {
|
|
||||||
userFilter = { $ne: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackFilter = {
|
|
||||||
'statistics.recordedAt': dateFilter,
|
|
||||||
...(userFilter ? { author: userFilter } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackCount = await Track.find(trackFilter).count();
|
|
||||||
|
|
||||||
const publicTrackCount = await Track.find({
|
|
||||||
...trackFilter,
|
|
||||||
public: true,
|
|
||||||
}).count();
|
|
||||||
|
|
||||||
const userCount = await User.find({
|
|
||||||
...(userFilter
|
|
||||||
? { _id: userFilter }
|
|
||||||
: {
|
|
||||||
createdAt: dateFilter,
|
|
||||||
}),
|
|
||||||
}).count();
|
|
||||||
|
|
||||||
const trackStats = await Track.aggregate([
|
|
||||||
{ $match: trackFilter },
|
|
||||||
{
|
|
||||||
$addFields: {
|
|
||||||
trackLength: {
|
|
||||||
$cond: [{ $lt: ['$statistics.length', 500000] }, '$statistics.length', 0],
|
|
||||||
},
|
|
||||||
numEvents: '$statistics.numEvents',
|
|
||||||
trackDuration: {
|
|
||||||
$cond: [
|
|
||||||
{ $and: ['$statistics.recordedUntil', { $gt: ['$statistics.recordedAt', new Date('2010-01-01')] }] },
|
|
||||||
{ $subtract: ['$statistics.recordedUntil', '$statistics.recordedAt'] },
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $project: { trackLength: true, numEvents: true, trackDuration: true } },
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: 'sum',
|
|
||||||
trackLength: { $sum: '$trackLength' },
|
|
||||||
numEvents: { $sum: '$numEvents' },
|
|
||||||
trackDuration: { $sum: '$trackDuration' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [trackLength, numEvents, trackDuration] =
|
|
||||||
trackStats.length > 0
|
|
||||||
? [trackStats[0].trackLength, trackStats[0].numEvents, trackStats[0].trackDuration]
|
|
||||||
: [0, 0, 0];
|
|
||||||
|
|
||||||
const trackLengthPrivatized = Math.floor(trackLength / TRACK_LENGTH_ROUNDING) * TRACK_LENGTH_ROUNDING;
|
|
||||||
const trackDurationPrivatized =
|
|
||||||
Math.round(trackDuration / 1000 / TRACK_DURATION_ROUNDING) * TRACK_DURATION_ROUNDING;
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
publicTrackCount,
|
|
||||||
trackLength: trackLengthPrivatized,
|
|
||||||
trackDuration: trackDurationPrivatized,
|
|
||||||
numEvents,
|
|
||||||
trackCount,
|
|
||||||
userCount,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,14 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
|
||||||
const { Track } = require('../../models');
|
|
||||||
|
|
||||||
// return a list of tags
|
|
||||||
router.get(
|
|
||||||
'/',
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const tags = await Track.find().distinct('tagList');
|
|
||||||
return res.json({ tags: tags });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,416 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const router = require('express').Router();
|
|
||||||
const { Track, User, Comment } = require('../../models');
|
|
||||||
const busboy = require('connect-busboy');
|
|
||||||
const auth = require('../../passport');
|
|
||||||
const { normalizeUserAgent, buildObsver1 } = require('../../logic/tracks');
|
|
||||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
|
||||||
const { PROCESSING_OUTPUT_DIR } = require('../../paths');
|
|
||||||
|
|
||||||
function preloadByParam(target, getValueFromParam) {
|
|
||||||
return async (req, res, next, paramValue) => {
|
|
||||||
try {
|
|
||||||
const value = await getValueFromParam(paramValue);
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
req[target] = value;
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
router.param(
|
|
||||||
'track',
|
|
||||||
preloadByParam('track', (slug) => Track.findOne({ slug }).populate('author')),
|
|
||||||
);
|
|
||||||
router.param(
|
|
||||||
'comment',
|
|
||||||
preloadByParam('comment', (id) => Comment.findById(id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.param('comment', async (req, res, next, id) => {
|
|
||||||
try {
|
|
||||||
const comment = await Comment.findById(id);
|
|
||||||
|
|
||||||
if (!comment) {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.comment = comment;
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const query = { public: true };
|
|
||||||
let limit = 20;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
if (typeof req.query.limit !== 'undefined') {
|
|
||||||
limit = req.query.limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof req.query.offset !== 'undefined') {
|
|
||||||
offset = req.query.offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof req.query.tag !== 'undefined') {
|
|
||||||
query.tagList = { $in: [req.query.tag] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = req.query.author ? await User.findOne({ username: req.query.author }) : null;
|
|
||||||
|
|
||||||
if (author) {
|
|
||||||
query.author = author._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tracks, tracksCount] = await Promise.all([
|
|
||||||
Track.find(query)
|
|
||||||
.sort('-createdAt')
|
|
||||||
.limit(Number(limit))
|
|
||||||
.skip(Number(offset))
|
|
||||||
.sort({ createdAt: 'desc' })
|
|
||||||
.populate('author')
|
|
||||||
.exec(),
|
|
||||||
Track.countDocuments(query).exec(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
tracks: tracks.map((track) => track.toJSONFor(req.user)),
|
|
||||||
tracksCount,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/feed',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
let limit = 20;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
if (typeof req.query.limit !== 'undefined') {
|
|
||||||
limit = req.query.limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof req.query.offset !== 'undefined') {
|
|
||||||
offset = req.query.offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = { author: req.user.id };
|
|
||||||
const [tracks, tracksCount] = await Promise.all([
|
|
||||||
Track.find(query).sort('-createdAt').limit(Number(limit)).skip(Number(offset)).populate('author').exec(),
|
|
||||||
Track.countDocuments(query),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
tracks: tracks.map(function (track) {
|
|
||||||
return track.toJSONFor(req.user);
|
|
||||||
}),
|
|
||||||
tracksCount: tracksCount,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
async function readFile(file) {
|
|
||||||
let fileContent = '';
|
|
||||||
|
|
||||||
file.on('data', function (data) {
|
|
||||||
fileContent += data;
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
file.on('end', resolve);
|
|
||||||
file.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
return fileContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMultipartOrJsonBody(req, mapJsonBody = (x) => x) {
|
|
||||||
const fileInfo = {};
|
|
||||||
let body;
|
|
||||||
|
|
||||||
if (req.busboy) {
|
|
||||||
body = {};
|
|
||||||
|
|
||||||
req.busboy.on('file', async function (fieldname, file, filename, encoding, mimetype) {
|
|
||||||
body[fieldname] = await readFile(file);
|
|
||||||
fileInfo[fieldname] = { filename, encoding, mimetype };
|
|
||||||
});
|
|
||||||
|
|
||||||
req.busboy.on('field', (key, value) => {
|
|
||||||
body[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
req.pipe(req.busboy);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
req.busboy.on('finish', resolve);
|
|
||||||
req.busboy.on('error', reject);
|
|
||||||
});
|
|
||||||
} else if (req.headers['content-type'] === 'application/json') {
|
|
||||||
body = mapJsonBody(req.body);
|
|
||||||
} else {
|
|
||||||
body = { body: await readFile(req), ...req.query };
|
|
||||||
fileInfo.body = {
|
|
||||||
mimetype: req.headers['content-type'],
|
|
||||||
filename: req.headers['content-disposition'],
|
|
||||||
encoding: req.headers['content-encoding'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { body, fileInfo };
|
|
||||||
}
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/',
|
|
||||||
auth.requiredWithUserId,
|
|
||||||
busboy(), // parse multipart body
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
// Read the whole file into memory. This is not optimal, instead, we should
|
|
||||||
// write the file data directly to the target file. However, we first have
|
|
||||||
// to parse the rest of the track data to know where to place the file.
|
|
||||||
// TODO: Stream into temporary file, then move it later.
|
|
||||||
const { body, fileInfo } = await getMultipartOrJsonBody(req, (body) => body.track);
|
|
||||||
|
|
||||||
const { body: fileBody, public, ...trackBody } = body;
|
|
||||||
|
|
||||||
const track = new Track({
|
|
||||||
...trackBody,
|
|
||||||
author: req.user,
|
|
||||||
public: public == null ? req.user.areTracksVisibleForAll : Boolean(trackBody.public),
|
|
||||||
});
|
|
||||||
track.customizedTitle = track.title != null;
|
|
||||||
track.slugify();
|
|
||||||
|
|
||||||
if (fileBody) {
|
|
||||||
await track.validateFileBodyUniqueness(fileBody);
|
|
||||||
track.uploadedByUserAgent = normalizeUserAgent(req.headers['user-agent']);
|
|
||||||
track.originalFileName = fileInfo.body ? fileInfo.body.filename : track.slug + '.csv';
|
|
||||||
await track.writeToOriginalFile(fileBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.save();
|
|
||||||
|
|
||||||
if (fileBody) {
|
|
||||||
await track.queueProcessing();
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.autoGenerateTitle();
|
|
||||||
|
|
||||||
return res.json({ track: track.toJSONFor(req.user) });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// return a track
|
|
||||||
router.get(
|
|
||||||
'/:track',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (!req.track.isVisibleTo(req.user)) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({ track: req.track.toJSONFor(req.user) });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// update track
|
|
||||||
router.put(
|
|
||||||
'/:track',
|
|
||||||
busboy(),
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const track = req.track;
|
|
||||||
|
|
||||||
if (!track.author._id.equals(req.user.id)) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: { body: fileBody, ...trackBody },
|
|
||||||
fileInfo,
|
|
||||||
} = await getMultipartOrJsonBody(req, (body) => body.track);
|
|
||||||
|
|
||||||
if (typeof trackBody.title !== 'undefined') {
|
|
||||||
track.title = (trackBody.title || '').trim() || null;
|
|
||||||
track.customizedTitle = track.title != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof trackBody.description !== 'undefined') {
|
|
||||||
track.description = (trackBody.description || '').trim() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let process = false;
|
|
||||||
|
|
||||||
if (trackBody.public != null) {
|
|
||||||
const public = Boolean(trackBody.public);
|
|
||||||
process |= public !== track.public;
|
|
||||||
track.public = public;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileBody) {
|
|
||||||
await track.validateFileBodyUniqueness(fileBody);
|
|
||||||
track.originalFileName = fileInfo.body ? fileInfo.body.filename : track.slug + '.csv';
|
|
||||||
track.uploadedByUserAgent = normalizeUserAgent(req.headers['user-agent']);
|
|
||||||
await track.writeToOriginalFile(fileBody);
|
|
||||||
process = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.save();
|
|
||||||
|
|
||||||
if (process) {
|
|
||||||
await track.queueProcessing();
|
|
||||||
}
|
|
||||||
|
|
||||||
await track.autoGenerateTitle();
|
|
||||||
|
|
||||||
return res.json({ track: track.toJSONFor(req.user) });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete track
|
|
||||||
router.delete(
|
|
||||||
'/:track',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (req.track.author._id.equals(req.user.id)) {
|
|
||||||
await req.track.remove();
|
|
||||||
return res.sendStatus(204);
|
|
||||||
} else {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// return an track's comments
|
|
||||||
router.get(
|
|
||||||
'/:track/comments',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (!req.track.isVisibleTo(req.user)) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.track.populate({
|
|
||||||
path: 'comments',
|
|
||||||
populate: {
|
|
||||||
path: 'author',
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
sort: {
|
|
||||||
createdAt: 'asc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
comments: req.track.comments.map(function (comment) {
|
|
||||||
return comment.toJSONFor(req.user);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// create a new comment
|
|
||||||
router.post(
|
|
||||||
'/:track/comments',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const comment = new Comment(req.body.comment);
|
|
||||||
comment.track = req.track;
|
|
||||||
comment.author = req.user;
|
|
||||||
|
|
||||||
await comment.save();
|
|
||||||
|
|
||||||
req.track.comments.push(comment);
|
|
||||||
|
|
||||||
await req.track.save();
|
|
||||||
return res.json({ comment: comment.toJSONFor(req.user) });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
'/:track/comments/:comment',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (req.comment.author.equals(req.user.id)) {
|
|
||||||
req.track.comments.remove(req.comment._id);
|
|
||||||
await req.track.save();
|
|
||||||
await Comment.find({ _id: req.comment._id }).remove();
|
|
||||||
res.sendStatus(204);
|
|
||||||
} else {
|
|
||||||
res.sendStatus(403);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// return an track's generated data
|
|
||||||
router.get(
|
|
||||||
'/:track/data',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const FILE_BY_KEY = {
|
|
||||||
measurements: 'measurements.json',
|
|
||||||
overtakingEvents: 'overtakingEvents.json',
|
|
||||||
track: 'track.json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!req.track.isVisibleTo(req.user)) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
for (const [key, filename] of Object.entries(FILE_BY_KEY)) {
|
|
||||||
const filePath = path.join(PROCESSING_OUTPUT_DIR, req.track.filePath, filename);
|
|
||||||
|
|
||||||
let stats;
|
|
||||||
|
|
||||||
try {
|
|
||||||
stats = await fs.promises.stat(filePath);
|
|
||||||
} catch (err) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stats.isFile()) {
|
|
||||||
// file does not exist (yet)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.promises.readFile(filePath);
|
|
||||||
const contentJson = JSON.parse(content);
|
|
||||||
|
|
||||||
result[key] = contentJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(result);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// download the original file
|
|
||||||
router.get(
|
|
||||||
'/:track/download/original.csv',
|
|
||||||
auth.optional,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (!req.track.isVisibleToPrivate(req.user)) {
|
|
||||||
return res.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.download(req.track.getOriginalFilePath(), req.track.originalFileName);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,52 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
|
||||||
const auth = require('../../passport');
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/user',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
return res.json({ user: req.user.toAuthJSON() });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.put(
|
|
||||||
'/user',
|
|
||||||
auth.required,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const user = req.user;
|
|
||||||
|
|
||||||
// only update fields that were actually passed...
|
|
||||||
if (typeof req.body.user.username !== 'undefined') {
|
|
||||||
user.username = req.body.user.username;
|
|
||||||
}
|
|
||||||
if (typeof req.body.user.email !== 'undefined') {
|
|
||||||
user.email = req.body.user.email;
|
|
||||||
}
|
|
||||||
if (typeof req.body.user.bio !== 'undefined') {
|
|
||||||
user.bio = req.body.user.bio;
|
|
||||||
}
|
|
||||||
if (typeof req.body.user.image !== 'undefined') {
|
|
||||||
user.image = req.body.user.image;
|
|
||||||
}
|
|
||||||
if (typeof req.body.user.areTracksVisibleForAll !== 'undefined') {
|
|
||||||
user.areTracksVisibleForAll = req.body.user.areTracksVisibleForAll;
|
|
||||||
}
|
|
||||||
if (typeof req.body.user.password === 'string' && req.body.user.password !== '') {
|
|
||||||
user.setPassword(req.body.user.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.save();
|
|
||||||
return res.json({ user: user.toAuthJSON() });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove this at some point
|
|
||||||
router.post('/users/login',
|
|
||||||
auth.usernameAndPassword,
|
|
||||||
wrapRoute((req, res) => {
|
|
||||||
return res.json({ user: req.user.toAuthJSON() });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,558 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
const passport = require('passport');
|
|
||||||
const { URL } = require('url');
|
|
||||||
const { createChallenge } = require('pkce');
|
|
||||||
|
|
||||||
const { AuthorizationCode, AccessToken, RefreshToken } = require('../models');
|
|
||||||
const auth = require('../passport');
|
|
||||||
const wrapRoute = require('../_helpers/wrapRoute');
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
const baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
||||||
|
|
||||||
// Check whether the "bigScope" fully includes the "smallScope".
|
|
||||||
function scopeIncludes(smallScope, bigScope) {
|
|
||||||
const smallScopeParts = smallScope.split(/\s/);
|
|
||||||
const bigScopeParts = bigScope.split(/\s/);
|
|
||||||
return bigScopeParts.includes('*') || smallScopeParts.every((part) => bigScopeParts.includes(part));
|
|
||||||
}
|
|
||||||
|
|
||||||
function returnError(res, error, errorDescription = undefined, status = 400) {
|
|
||||||
return res
|
|
||||||
.status(status)
|
|
||||||
.json({ error, ...(errorDescription != null ? { error_description: errorDescription } : {}) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectWithParams(res, redirectUri, params) {
|
|
||||||
const targetUrl = new URL(redirectUri);
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
targetUrl.searchParams.append(key, value);
|
|
||||||
}
|
|
||||||
return res.redirect(targetUrl.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALL_SCOPE_NAMES = `
|
|
||||||
tracks.create
|
|
||||||
tracks.update
|
|
||||||
tracks.list
|
|
||||||
tracks.show
|
|
||||||
tracks.delete
|
|
||||||
users.update
|
|
||||||
users.show
|
|
||||||
tracks.comments.create
|
|
||||||
tracks.comments.update
|
|
||||||
tracks.comments.list
|
|
||||||
tracks.comments.show
|
|
||||||
`.split(/\s/);
|
|
||||||
|
|
||||||
function isValidScope(scope) {
|
|
||||||
return scope === '*' || scopeIncludes(scope, ALL_SCOPE_NAMES.join(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
|
||||||
res.locals.user = req.user;
|
|
||||||
res.locals.mainFrontendUrl = config.mainFrontendUrl;
|
|
||||||
res.locals.imprintUrl = config.imprintUrl;
|
|
||||||
res.locals.privacyPolicyUrl = config.privacyPolicyUrl;
|
|
||||||
res.locals.baseUrl = baseUrl + '/';
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/login',
|
|
||||||
passport.authenticate('usernameAndPasswordSession', { session: true }),
|
|
||||||
(err, req, res, next) => {
|
|
||||||
if (!err) {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.message === 'invalid credentials') {
|
|
||||||
return res.render('login', { badCredentials: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
let description = 'Unknown error while processing your login.';
|
|
||||||
if (err.message === 'email not verified') {
|
|
||||||
description = 'Your account is not yet verified, please check your email or start the password recovery.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.render('message', { type: 'negative', title: 'Login failed', description });
|
|
||||||
},
|
|
||||||
wrapRoute((req, res, next) => {
|
|
||||||
if (!req.user) {
|
|
||||||
return res.redirect(baseUrl + '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.session.next) {
|
|
||||||
res.redirect(baseUrl + req.session.next);
|
|
||||||
req.session.next = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return res.render('message', { type: 'positive', title: 'You are logged in.', showFrontendLink: true });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/login',
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (req.user) {
|
|
||||||
return res.render('message', { type: 'positive', title: 'You are already logged in.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.render('login');
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router
|
|
||||||
.route('/logout')
|
|
||||||
.post(
|
|
||||||
auth.usernameAndPasswordSession,
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
req.logout();
|
|
||||||
return res.redirect(baseUrl + '/login');
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.get((req, res) => {
|
|
||||||
if (req.query.redirectTo) {
|
|
||||||
req.logout();
|
|
||||||
return res.redirect(req.query.redirectTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.render('logout')
|
|
||||||
});
|
|
||||||
|
|
||||||
const isIp = (ip) =>
|
|
||||||
typeof ip === 'string' &&
|
|
||||||
/^([0-9]{1,3}\.)[0-9]{1,3}$/.test(ip) &&
|
|
||||||
ip
|
|
||||||
.split('.')
|
|
||||||
.every(
|
|
||||||
(num, idx) =>
|
|
||||||
!num.startsWith('0') && Number(num) > (idx === 0 || idx === 3 ? 1 : 0) && Number(num) < (idx === 3 ? 254 : 255),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLocalIp = (ip) => isIp(ip) && (ip.startsWith('10.') || ip.startsWith('172.16.') || ip.startsWith('192.168.'));
|
|
||||||
|
|
||||||
const isValidRedirectUriFor = (redirectUri) => (redirectUriPattern) => {
|
|
||||||
// Here we have an exception to the security requirements demanded by
|
|
||||||
// https://tools.ietf.org/html/draft-ietf-oauth-security-topics-16#section-2.1,
|
|
||||||
// namely, that we do not always require fully specified redirect URIs. This
|
|
||||||
// is because we cannot know beforehand which IP the OBS will be running at.
|
|
||||||
// But since it is usually accessed via local IP, we can allow all local IP
|
|
||||||
// ranges. This special case must only be used in clients that have a very
|
|
||||||
// restricted `maxScope` as well, to prevent misuse should an attack through
|
|
||||||
// this be successful.
|
|
||||||
// This special case does however enforce TLS ("https://"), for it prevents
|
|
||||||
// usage in a non-TLS-secured web server. At least passive sniffing of the
|
|
||||||
// token is not possible then. A self-signed and manually verified
|
|
||||||
// certificate should be used for this (though usually we cannot enforce the
|
|
||||||
// actual verification).
|
|
||||||
if (redirectUriPattern === '__LOCAL__') {
|
|
||||||
const url = new URL(redirectUri);
|
|
||||||
if (url.protocol === 'https:' && isLocalIp(url.host) && !url.search && !url.hash) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return redirectUriPattern === redirectUri;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/authorize',
|
|
||||||
passport.authenticate('session'),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (!req.user) {
|
|
||||||
req.session.next = req.url;
|
|
||||||
return res.redirect(baseUrl + '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
client_id: clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
response_type: responseType,
|
|
||||||
scope = '*', // fallback to "all" scope
|
|
||||||
// for PKCE
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
code_challenge_method: codeChallengeMethod,
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
// 1. Find our client and check if it exists
|
|
||||||
if (!clientId) {
|
|
||||||
return returnError(res, 'invalid_request', 'client_id parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = config.oAuth2Clients.find((c) => c.clientId === clientId);
|
|
||||||
if (!client) {
|
|
||||||
return returnError(res, 'invalid_client', 'unknown client');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Check that we have a redirect_uri. In addition to [RFC6749] we
|
|
||||||
// *always* require a redirect_uri.
|
|
||||||
if (!redirectUri) {
|
|
||||||
return returnError(res, 'invalid_request', 'redirect_uri parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// We enforce that the redirectUri exactly matches one of the provided URIs
|
|
||||||
const check = isValidRedirectUriFor(redirectUri);
|
|
||||||
if (!client.validRedirectUris.some(check)) {
|
|
||||||
return returnError(res, 'invalid_request', 'invalid redirect_uri');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Find out which type of response to use. [RFC6749] requires one of
|
|
||||||
// "code" or "token", but "token" is implicit grant and we do not support
|
|
||||||
// that.
|
|
||||||
|
|
||||||
if (responseType !== 'code') {
|
|
||||||
return redirectWithParams(res, redirectUri, {
|
|
||||||
error: 'unsupported_grant_type',
|
|
||||||
error_description: 'only authorization code flow with PKCE is supported by this server',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Verify we're using PKCE with supported (S256) code_challenge_method.
|
|
||||||
if (!codeChallenge) {
|
|
||||||
return redirectWithParams(res, redirectUri, {
|
|
||||||
error: 'invalid_request',
|
|
||||||
error_description: 'a code_challenge for PKCE is required',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeChallengeMethod !== 'S256') {
|
|
||||||
return redirectWithParams(res, redirectUri, {
|
|
||||||
error: 'invalid_request',
|
|
||||||
error_description: 'the code_challenge_method for PKCE must be "S256"',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Get the scope.
|
|
||||||
if (!isValidScope(scope)) {
|
|
||||||
return redirectWithParams(res, redirectUri, {
|
|
||||||
error: 'invalid_scope',
|
|
||||||
error_description: 'the requested scope is not known',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.maxScope && !scopeIncludes(scope, client.maxScope)) {
|
|
||||||
return redirectWithParams(res, redirectUri, {
|
|
||||||
error: 'access_denied',
|
|
||||||
error_description: 'the requested scope is not valid for this client',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok, let's save all this in the session, and show a dialog for the
|
|
||||||
// decision to the user.
|
|
||||||
//
|
|
||||||
if (client.autoAccept) {
|
|
||||||
const code = AuthorizationCode.generate({
|
|
||||||
clientId,
|
|
||||||
user: req.user,
|
|
||||||
redirectUri,
|
|
||||||
scope,
|
|
||||||
codeChallenge,
|
|
||||||
});
|
|
||||||
await code.save();
|
|
||||||
|
|
||||||
return redirectWithParams(res, redirectUri, { code: code.code, scope });
|
|
||||||
} else {
|
|
||||||
req.session.authorizationTransaction = {
|
|
||||||
responseType,
|
|
||||||
clientId,
|
|
||||||
redirectUri,
|
|
||||||
scope,
|
|
||||||
expiresAt: new Date().getTime() + 1000 * 60 * 2, // 2 minute decision time
|
|
||||||
codeChallenge,
|
|
||||||
};
|
|
||||||
|
|
||||||
res.render('authorize', { clientTitle: client.title, scope, redirectUri });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400).json({ error: 'invalid_request', error_description: 'unknown error' });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
['/authorize/approve', '/authorize/decline'],
|
|
||||||
passport.authenticate('session'),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
if (!req.session.authorizationTransaction) {
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.user) {
|
|
||||||
return res.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { clientId, redirectUri, scope, expiresAt, codeChallenge } = req.session.authorizationTransaction;
|
|
||||||
|
|
||||||
if (expiresAt < new Date().getTime()) {
|
|
||||||
return res.status(400).render('message', {
|
|
||||||
type: 'negative',
|
|
||||||
title: 'Expired',
|
|
||||||
description: 'Your authorization has expired. Please go back and retry the process.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// invalidate the transaction
|
|
||||||
req.session.authorizationTransaction = null;
|
|
||||||
|
|
||||||
if (req.path === '/authorize/approve') {
|
|
||||||
const code = AuthorizationCode.generate({
|
|
||||||
clientId,
|
|
||||||
user: req.user,
|
|
||||||
redirectUri,
|
|
||||||
scope,
|
|
||||||
codeChallenge,
|
|
||||||
});
|
|
||||||
await code.save();
|
|
||||||
|
|
||||||
return redirectWithParams(res, redirectUri, { code: code.code, scope });
|
|
||||||
} else {
|
|
||||||
return redirectWithParams(res, redirectUri, { error: 'access_denied' });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called when the client presents an authorization code
|
|
||||||
* (generated above) and wants it turned into an access (and possibly refresh)
|
|
||||||
* token.
|
|
||||||
*/
|
|
||||||
router.get(
|
|
||||||
'/token',
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const {
|
|
||||||
grant_type: grantType,
|
|
||||||
code,
|
|
||||||
client_id: clientId,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
// for PKCE
|
|
||||||
code_verifier: codeVerifier,
|
|
||||||
} = req.query;
|
|
||||||
|
|
||||||
if (!grantType || grantType !== 'authorization_code') {
|
|
||||||
return returnError(
|
|
||||||
res,
|
|
||||||
'unsupported_grant_type',
|
|
||||||
'only authorization code flow with PKCE is supported by this server',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
return returnError(res, 'invalid_request', 'code parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call this function to destroy the authorization code (if it exists),
|
|
||||||
// invalidating it when a single failed request has been received. The
|
|
||||||
// whole process must be restarted. No trial and error ;)
|
|
||||||
const destroyAuthCode = async () => {
|
|
||||||
await AuthorizationCode.deleteOne({ code });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!clientId) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_client', 'client_id parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!redirectUri) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_request', 'redirect_uri parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!codeVerifier) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_request', 'code_verifier parameter required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = config.oAuth2Clients.find((c) => c.clientId === clientId);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_client', 'invalid client_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorizationCode = await AuthorizationCode.findOne({ code });
|
|
||||||
if (!authorizationCode) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
|
||||||
}
|
|
||||||
if (authorizationCode.redirectUri !== redirectUri) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
|
||||||
}
|
|
||||||
if (authorizationCode.expiresAt <= new Date().getTime()) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
|
||||||
}
|
|
||||||
if (clientId !== authorizationCode.clientId) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
|
||||||
}
|
|
||||||
if (createChallenge(codeVerifier) !== authorizationCode.codeChallenge) {
|
|
||||||
await destroyAuthCode();
|
|
||||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
|
||||||
}
|
|
||||||
|
|
||||||
// invalidate auth code now, before generating tokens
|
|
||||||
await AuthorizationCode.deleteOne({ _id: authorizationCode._id });
|
|
||||||
|
|
||||||
const accessToken = AccessToken.generate({
|
|
||||||
clientId: authorizationCode.clientId,
|
|
||||||
user: authorizationCode.user,
|
|
||||||
scope: authorizationCode.scope,
|
|
||||||
});
|
|
||||||
await accessToken.save();
|
|
||||||
|
|
||||||
let refreshToken;
|
|
||||||
if (client.refreshTokenExpirySeconds != null) {
|
|
||||||
refreshToken = RefreshToken.generate(
|
|
||||||
{
|
|
||||||
clientId: authorizationCode.clientId,
|
|
||||||
user: authorizationCode.user,
|
|
||||||
scope: authorizationCode.scope,
|
|
||||||
},
|
|
||||||
client.refreshTokenExpirySeconds,
|
|
||||||
);
|
|
||||||
await refreshToken.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
access_token: accessToken.token,
|
|
||||||
token_type: 'Bearer',
|
|
||||||
expires_in: Math.round((accessToken.expiresAt - new Date().getTime()) / 1000),
|
|
||||||
scope: accessToken.scope,
|
|
||||||
...(refreshToken != null ? { refresh_token: refreshToken.token } : {}),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata endpoint to inform clients about authorization server capabilities,
|
|
||||||
* according to https://tools.ietf.org/html/rfc8414.
|
|
||||||
*/
|
|
||||||
router.get(
|
|
||||||
'/.well-known/oauth-authorization-server',
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
return res.json({
|
|
||||||
issuer: baseUrl,
|
|
||||||
authorization_endpoint: `${baseUrl}/authorize`,
|
|
||||||
token_endpoint: `${baseUrl}/token`,
|
|
||||||
token_endpoint_auth_methods_supported: ['none'], // only public clients
|
|
||||||
userinfo_endpoint: `${baseUrl}/api/user`,
|
|
||||||
// registration_endpoint: `${baseUrl}/register`, // TODO
|
|
||||||
// scopes_supported: ALL_SCOPE_NAMES, // TODO
|
|
||||||
response_types_supported: ['code'], // only auth code, no implicit flow or
|
|
||||||
service_documentation: 'https://github.com/openbikesensor/portal',
|
|
||||||
ui_locales_supported: ['en-US', 'en-GB', 'en-CA', 'fr-FR', 'fr-CA'],
|
|
||||||
code_challenge_methods_supported: ['S256'],
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
|
|
||||||
const accountService = require('../accounts/account.service');
|
|
||||||
const validateRequest = require('../_middleware/validate-request');
|
|
||||||
const Joi = require('joi');
|
|
||||||
|
|
||||||
router
|
|
||||||
.route('/register')
|
|
||||||
.post(
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
username: Joi.string().required(),
|
|
||||||
email: Joi.string().email().required(),
|
|
||||||
password: Joi.string().min(6).required(),
|
|
||||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
|
||||||
|
|
||||||
...(config.privacyPolicyUrl ? {
|
|
||||||
acceptPrivacyPolicy: Joi.boolean().truthy().required(),
|
|
||||||
} : {}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.register(req.body);
|
|
||||||
|
|
||||||
return res.render('message', {
|
|
||||||
type: 'positive',
|
|
||||||
title: 'Registration successful',
|
|
||||||
description: 'Please check your email for verification instructions.',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.get((req, res) => res.render('register'));
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/verify-email',
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
'query',
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.verifyEmail(req.query);
|
|
||||||
return res.render('message', {
|
|
||||||
type: 'positive',
|
|
||||||
title: 'Verification successful',
|
|
||||||
description: 'You can now log in.',
|
|
||||||
showLoginButton: true,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
router
|
|
||||||
.route('/forgot-password')
|
|
||||||
.post(
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
email: Joi.string().email().required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.forgotPassword(req.body);
|
|
||||||
res.render('message', {
|
|
||||||
type: 'positive',
|
|
||||||
title: 'Recovery mail sent',
|
|
||||||
description: 'Please check your inbox for password recovery instructions.',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.get((req, res) => res.render('forgot-password'));
|
|
||||||
|
|
||||||
router
|
|
||||||
.route('/reset-password')
|
|
||||||
.post(
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
password: Joi.string().min(6).required(),
|
|
||||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
await accountService.resetPassword(req.body);
|
|
||||||
return res.render('message', {
|
|
||||||
type: 'positive',
|
|
||||||
title: 'Password reset successful',
|
|
||||||
description: 'You can now log in.',
|
|
||||||
showLoginButton: true,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
validateRequest(
|
|
||||||
Joi.object({
|
|
||||||
token: Joi.string().required(),
|
|
||||||
}),
|
|
||||||
'query',
|
|
||||||
),
|
|
||||||
wrapRoute(async (req, res) => {
|
|
||||||
const { token } = req.query;
|
|
||||||
await accountService.validateResetToken({ token });
|
|
||||||
res.render('reset-password', { token });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,8 +0,0 @@
|
||||||
const router = require('express').Router();
|
|
||||||
|
|
||||||
router.use('/api', require('./api'));
|
|
||||||
|
|
||||||
// no prefix
|
|
||||||
router.use(require('./auth'));
|
|
||||||
|
|
||||||
module.exports = router;
|
|
|
@ -1,137 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
|
|
||||||
const queue = require('./queue');
|
|
||||||
require('./db');
|
|
||||||
const { Track } = require('./models');
|
|
||||||
const { API_ROOT_DIR, PROCESSING_DIR, OBS_FACE_CACHE_DIR, PROCESSING_OUTPUT_DIR } = require('./paths');
|
|
||||||
const config = require('./config');
|
|
||||||
|
|
||||||
queue.process('processTrack', async (job) => {
|
|
||||||
const track = await Track.findById(job.data.trackId);
|
|
||||||
if (!track) {
|
|
||||||
throw new Error('Cannot find track to process');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.processingJobId !== job.id) {
|
|
||||||
throw new Error('Track is processed by another job');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.processingJobId !== job.id) {
|
|
||||||
throw new Error('Track is processed by another job');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (track.processingStatus !== 'pending') {
|
|
||||||
throw new Error('Track is not pending processing');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { filePath } = track;
|
|
||||||
console.log('Will process track', filePath);
|
|
||||||
|
|
||||||
track.processingStatus = 'processing';
|
|
||||||
track.processingLog = '';
|
|
||||||
await track.save();
|
|
||||||
|
|
||||||
// Create input directory
|
|
||||||
const inputDirectory = path.join(PROCESSING_DIR, filePath);
|
|
||||||
await fs.promises.mkdir(inputDirectory, { recursive: true });
|
|
||||||
|
|
||||||
// copy original file to processing dir
|
|
||||||
const inputFilePath = path.join(inputDirectory, 'track.csv');
|
|
||||||
const originalFilePath = track.getOriginalFilePath();
|
|
||||||
console.log(`[${track.slug}] Copy ${originalFilePath} to ${inputFilePath}`);
|
|
||||||
await fs.promises.copyFile(originalFilePath, inputFilePath);
|
|
||||||
|
|
||||||
// create track settings file
|
|
||||||
const settingsFilePath = path.join(inputDirectory, 'track-settings.json');
|
|
||||||
console.log(`[${track.slug}] Create settings at ${settingsFilePath}`);
|
|
||||||
const settings = {
|
|
||||||
trackId: String(track._id),
|
|
||||||
settingsGeneratedAt: new Date().getTime(),
|
|
||||||
filters: [
|
|
||||||
// TODO: Add actual privacy zones from user database
|
|
||||||
/* {
|
|
||||||
type: 'PrivacyZonesFilter',
|
|
||||||
config: { privacyZones: [{ longitude: 10, latitude: 10, radius: 250 }] },
|
|
||||||
}, */
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await fs.promises.writeFile(settingsFilePath, JSON.stringify(settings));
|
|
||||||
|
|
||||||
// Create output directory
|
|
||||||
const outputDirectory = path.join(PROCESSING_OUTPUT_DIR, filePath);
|
|
||||||
await fs.promises.mkdir(outputDirectory, { recursive: true });
|
|
||||||
|
|
||||||
const stdoutFile = path.join(outputDirectory, 'stdout.log');
|
|
||||||
const stderrFile = path.join(outputDirectory, 'stderr.log');
|
|
||||||
const stdout = fs.createWriteStream(stdoutFile);
|
|
||||||
const stderr = fs.createWriteStream(stderrFile);
|
|
||||||
|
|
||||||
// TODO: Generate track transformation settings (privacy zones etc)
|
|
||||||
// const settingsFilePath = path.join(inputDirectory, 'track-settings.json');
|
|
||||||
const child = spawn(
|
|
||||||
'python3',
|
|
||||||
[
|
|
||||||
path.join(API_ROOT_DIR, 'tools', 'process_track.py'),
|
|
||||||
'--input',
|
|
||||||
inputFilePath,
|
|
||||||
'--output',
|
|
||||||
outputDirectory,
|
|
||||||
'--path-cache',
|
|
||||||
OBS_FACE_CACHE_DIR,
|
|
||||||
'--settings',
|
|
||||||
settingsFilePath,
|
|
||||||
// '--anonymize-user-id', 'remove',
|
|
||||||
// '--anonymize-measurement-id', 'remove',
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: PROCESSING_DIR,
|
|
||||||
env: {
|
|
||||||
POSTGRES_URL: config.postgres.url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
child.stdout.pipe(process.stdout);
|
|
||||||
child.stdout.pipe(stdout);
|
|
||||||
child.stderr.pipe(process.stderr);
|
|
||||||
child.stderr.pipe(stderr);
|
|
||||||
|
|
||||||
const code = await new Promise((resolve) => child.on('close', resolve));
|
|
||||||
|
|
||||||
track.processingLog += (
|
|
||||||
await Promise.all([
|
|
||||||
fs.promises.readFile(stdoutFile),
|
|
||||||
fs.promises.readFile(stderrFile),
|
|
||||||
// split lines
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.join('\n')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
if (code !== 0) {
|
|
||||||
throw new Error(`Track processing failed with status ${code}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read some results back into the database for quick access and
|
|
||||||
// accumulation
|
|
||||||
const statisticsContent = await fs.promises.readFile(path.join(outputDirectory, 'statistics.json'));
|
|
||||||
track.statistics = JSON.parse(statisticsContent);
|
|
||||||
|
|
||||||
track.processingStatus = 'complete';
|
|
||||||
await track.save();
|
|
||||||
|
|
||||||
// Maybe we have found out the recording date, regenerate the automatic
|
|
||||||
// title (if not yet manually set)
|
|
||||||
await track.autoGenerateTitle();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Processing failed:', err);
|
|
||||||
track.processingLog += String(err) + '\n' + err.stack + '\n';
|
|
||||||
track.processingStatus = 'error';
|
|
||||||
await track.save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Worker started.');
|
|
|
@ -1,17 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Authorize #{clientTitle}
|
|
||||||
|
|
||||||
block content
|
|
||||||
p.
|
|
||||||
You are about to confirm a login to client #[b= clientTitle]. You have 2
|
|
||||||
minutes time for your decision.
|
|
||||||
|
|
||||||
.ui.grid.two.columns
|
|
||||||
form.column(method="post", action="authorize/decline")
|
|
||||||
button.ui.button.fluid.red(type="submit", class="red") Decline
|
|
||||||
|
|
||||||
form.column(method="post", action="authorize/approve")
|
|
||||||
button.ui.button.fluid.green(type="submit", class="green") Approve
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Reset Password
|
|
||||||
|
|
||||||
block content
|
|
||||||
form.ui.form(method="post")
|
|
||||||
.field
|
|
||||||
label(for="email") E-Mail Address
|
|
||||||
input(id="email", name="email")
|
|
||||||
|
|
||||||
button.ui.button.primary(type="submit") Send recovery mail
|
|
|
@ -1,34 +0,0 @@
|
||||||
html
|
|
||||||
head
|
|
||||||
base(href=baseUrl)
|
|
||||||
title
|
|
||||||
block title
|
|
||||||
| Authorization Server
|
|
||||||
| - OpenBikeSensor Account
|
|
||||||
|
|
||||||
link(rel="stylesheet", href="semantic-ui/semantic.min.css")
|
|
||||||
|
|
||||||
style.
|
|
||||||
body > main > header {
|
|
||||||
margin: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body
|
|
||||||
main.ui.container
|
|
||||||
header
|
|
||||||
nav.ui.menu
|
|
||||||
a.item(href=mainFrontendUrl, title="Back to Portal")
|
|
||||||
i.icon.arrow.left
|
|
||||||
.header.item OpenBikeSensor Account
|
|
||||||
.right.menu
|
|
||||||
if mainFrontendUrl
|
|
||||||
if !user
|
|
||||||
a.item(href="login") Login
|
|
||||||
a.item(href="register") Register
|
|
||||||
if user
|
|
||||||
a.item(href="logout") Logout
|
|
||||||
div.ui.grid.centered
|
|
||||||
div.eight.wide.column
|
|
||||||
h2.ui.header
|
|
||||||
block title
|
|
||||||
block content
|
|
|
@ -1,20 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Login
|
|
||||||
|
|
||||||
block content
|
|
||||||
form.ui.form(method="post")
|
|
||||||
.field
|
|
||||||
label(for="email") E-Mail Address
|
|
||||||
input(id="email", name="email")
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="password") Password
|
|
||||||
input(name="password", type="password")
|
|
||||||
|
|
||||||
if badCredentials
|
|
||||||
p.ui.negative.message Invalid login credentials, please try again.
|
|
||||||
|
|
||||||
button.ui.button.primary(type="submit", style="margin-right: 2rem") Login
|
|
||||||
a.ui(href="forgot-password") I forgot my password
|
|
|
@ -1,8 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Logout
|
|
||||||
|
|
||||||
block content
|
|
||||||
form(method="post", action="logout")
|
|
||||||
button.ui.button(type="submit") Log out now
|
|
|
@ -1,14 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
= title
|
|
||||||
|
|
||||||
block content
|
|
||||||
if description
|
|
||||||
div(class="ui message " + type)= description
|
|
||||||
|
|
||||||
if showLoginButton
|
|
||||||
p: a(href="login") Go to login
|
|
||||||
|
|
||||||
if showFrontendLink
|
|
||||||
p: a(href=mainFrontendUrl) Back to Portal
|
|
|
@ -1,38 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Register
|
|
||||||
|
|
||||||
block content
|
|
||||||
form.ui.form(method="post")
|
|
||||||
.field
|
|
||||||
label(for="username") Username
|
|
||||||
input(id="username", name="username")
|
|
||||||
p.form-hint At least 3 characters, alphanumerical, must be unique
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="email") E-Mail Address
|
|
||||||
input(id="email", name="email", type="email")
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="password") Password
|
|
||||||
input(id="password", name="password", type="password")
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="confirmPassword") Confirm Password
|
|
||||||
input(id="confirmPassword", name="confirmPassword", type="password")
|
|
||||||
|
|
||||||
if privacyPolicyUrl
|
|
||||||
.field
|
|
||||||
label Privacy policy
|
|
||||||
|
|
||||||
.ui.checkbox
|
|
||||||
input(id="acceptPrivacyPolicy", name="acceptPrivacyPolicy", type="checkbox", value="true")
|
|
||||||
label(for="acceptPrivacyPolicy")
|
|
||||||
| I have read and understood the
|
|
||||||
|
|
|
||||||
a(href=privacyPolicyUrl, target="_blank", rel="noreferrer") privacy policy
|
|
||||||
|
|
|
||||||
| and consent to the use of my data as described therein.
|
|
||||||
|
|
||||||
button.ui.button.primary(type="submit") Register
|
|
|
@ -1,18 +0,0 @@
|
||||||
extends layout.pug
|
|
||||||
|
|
||||||
block title
|
|
||||||
| Reset Password
|
|
||||||
|
|
||||||
block content
|
|
||||||
form.ui.form(method="post")
|
|
||||||
input(type="hidden", name="token", value=token)
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="password") New Password
|
|
||||||
input(id="password", name="password", type="password")
|
|
||||||
|
|
||||||
.field
|
|
||||||
label(for="confirmPassword") Confirm Password
|
|
||||||
input(id="confirmPassword", name="confirmPassword", type="password")
|
|
||||||
|
|
||||||
button.ui.button.primary(type="submit") Set new password
|
|
Loading…
Reference in a new issue