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