diff --git a/.eslintrc.json b/.eslintrc.json index f09bcb4..c69a5cb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,6 @@ //"@typescript-eslint/no-unused-vars": "warn" "jest/no-conditional-expect": "off" }, - "ignorePatterns": ["dist", "jest.config.js", "httpdocs", "webpack.config.js", "src/client"] + "ignorePatterns": ["dist", "jest.config.js", "httpdocs", "webpack.config.js", "src/client", "init"] } diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e31f256..caf06d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,8 +14,7 @@ jobs: NODE_ENV: ${{ vars.NODE_ENV }} LOCALHOST: ${{ vars.LOCALHOST }} LOCALHOSTV6: ${{ vars.LOCALHOSTV6 }} - KEYA: ${{ secrets.KEYA }} - KEYB: ${{ secrets.KEYB }} + KEY: ${{ secrets.KEY }} USER_TEST: ${{ secrets.USER_TEST }} steps: @@ -30,8 +29,8 @@ jobs: - run: npm run build --if-present - name: Start server run: | - sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEYA=$KEYA KEYB=$KEYB USER_TEST=$USER_TEST npm start & - sleep 15 # Give server some time to start + sudo NODE_ENV=$NODE_ENV LOCALHOST=$LOCALHOST LOCALHOSTV6=$LOCALHOSTV6 KEY=$KEY USER_TEST=$USER_TEST npm start & + sleep 16 # Give server some time to start - name: Check if server is running run: | curl --fail http://localhost:80 || exit 1 diff --git a/README.md b/README.md index 228b1b5..f415bf9 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,7 @@ Leaflet Osmand React ExpressJS Coordinates (X) > Remember an "X" marks the spot. ## Goal -**Technical:** Recieve and store coordinates via webhook and display them on a map through an interactive frontend. -**Personal:** The intend of this project is to get familiar with the listed technologies. - -## Progress -View [milestones](https://github.com/Type-Style/LOREX/milestones) and the [board](https://github.com/users/Type-Style/projects/1) to witness progression +Recieve and store coordinates via webhook and display them on a map through an interactive frontend. ## Installation ### Prerequisites @@ -20,6 +16,87 @@ Run the install command for example via npm `npm install` After completion without errors hit -`npx ts-node src/app.ts` +`npm run build` + +Once complete you can start the server using +`npm run start` + +> [!TIP] +Build and start can be combined using +`npm run dev` + +### Generating Key and Password +`Error Message: KEY is missing in environment variables` +Before you are able to login and use the webapplication, environment variables need to be setup. +Therefore you need to create a file `.env` and place it in the root of the project. +This file will be filled with secrets to protect your instance of LOREX. + +####1st. Generate Key +Usage: open console run: node init/generateKey.js +type desired key and hit enter +copy output to .env add a line starting with: +KEY= +directly followed by your output + +####2nd Generate Password(s) +Prerequisite: KEY already generated! +_(may require server restart)_ +Run the build command from the package.json (npm run build) +Then call the compiled version of this script using the key as environment variable like so: +KEY=your-key node ./init/generatePassword.js +Enter your password +Copy that to the Environment Variables and .env file +USER_WHATEVER= +followed by the output of the console + +> [!IMPORTANT] +In order to run automatic tests and create example data is is highly recommended to have a USER_TEST with the password of `test` +THe test user cannot be used in an production environment + +Once completed rebuild / restart the server and open up localhost/login +Login is now possible using the Username from the .env file in this example "WHATEVER" and the password that was created in the previous step. + +**Now the application is ready** + + +## Generating Data +### Example Data +Build and start the server for example using +`npm run dev` +Wait for the server to start and webpack to compile the assets. +Once done use a second command line to run +`npm run test:data` + +This generates 6 entries each 30s appart and calls the webserver to store this data. +The writing of data can be seen in the first console where the server is running. +Also once logged in under localhost, a map is visible showing a route westbound from the brandenburg gate. + +### Calling writing route manually +Data can be generated by calling the /write rout of the server. +Here is an example: + +`http://localhost/write?user=xx&lat=53.5000&lon=10.0×tamp=1720691648188&hdop=10.0&altitude=1000.000&speed=100.000&heading=180.0&key=test` + + +In order to pass validation use the correct key _(or `test` in development envrioment)_ and create a valid [UNIX timestamp]([url](https://currentmillis.com/)). +For example by using this javascript code: +`var a = new Date().getTime(); +copy(a); +a;` + +## Using on Production +### A note on security +This application is not developed with https built in support. +> [!WARNING] It is advised to run this application in `production` mode behind a proxy that uses https for security reasons + +### Getting data +Similar to the section Generating Data and Calling writing route manually, the application relies on data being provided using a webhook. +Well how data is collected and what data is pushed to the system is user preference. +Feel free to build you own application to do so. + +This application is designed to be used with the [OSMAND+ mobile app]([url](https://osmand.net/)). +Due to a plugin called [Triprecording]([url](https://osmand.net/docs/user/plugins/trip-recording/)) +Using the above link or by [clicking here](https://osmand.net/docs/user/plugins/trip-recording#recording-settings) more information can be found to setup webtracking or "online tracking" + -**Now the server is ready** + diff --git a/init/generateKey.js b/init/generateKey.js new file mode 100644 index 0000000..e2633f0 --- /dev/null +++ b/init/generateKey.js @@ -0,0 +1,30 @@ +/* +* Usage: open console run: node init/generateKey.js +* type desired key and hit enter +* copy output to .env add a line starting with: +* KEY= +* directly followed by your output +*/ + +// Import required modules +const readline = require('readline'); + +// set up readline to read input from the console +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Prompt user for input +rl.question('Enter the string to be encoded: ', (input) => { + // encode to escape special chars + const escapedString = encodeURIComponent(input); + + // convert the escaped string to base64 + const base64String = Buffer.from(escapedString).toString('base64'); + + // print the result + console.log('Base64 Encoded String:', base64String); + + rl.close(); +}); \ No newline at end of file diff --git a/init/generatePassword.js b/init/generatePassword.js new file mode 100644 index 0000000..c3b26b0 --- /dev/null +++ b/init/generatePassword.js @@ -0,0 +1,29 @@ +/* +* This is used to setup Passwords initially +* You can create passwords using the same logic as in the environment +* Prerequisite: You need to have KEY already generated! +* Run the build command from the package.json (npm run build) +* Then call the compiled version of this script using the key as environment variable like so: +* KEY=your-key node ./init/generatePassword.js +* Enter your password +* Copy that to the Environment Variables and .env file +* USER_WHATEVER= +* followed by the output of the console +*/ + +// Import required modules +const readline = require('readline'); +const { crypt } = require('../dist/scripts/crypt'); + +// Set up readline to read input from the console +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +// Prompt user for input +rl.question('Enter Password to be generated: ', async (input) => { + const cryptedPassword = await crypt(input); + console.log(cryptedPassword); + rl.close(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 740c503..7843c4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.16.6", + "@mui/icons-material": "^5.16.7", + "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", @@ -24,7 +24,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.1.0", + "express-validator": "^7.2.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -1697,9 +1697,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.16.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.5.tgz", - "integrity": "sha512-bn88xxU/J9UV0s6+eutq7o3TTOrOlbCX+KshFb8kxgIxJZZfYz3JbAXVMivvoMF4Md6jCVUzM9HEkf4Ajab4tw==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", + "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9" @@ -1723,14 +1723,14 @@ } }, "node_modules/@mui/material": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.6.tgz", - "integrity": "sha512-0LUIKBOIjiFfzzFNxXZBRAyr9UQfmTAFzbt6ziOU2FDXhorNN2o3N9/32mNJbCA8zJo2FqFU6d3dtoqUDyIEfA==", + "version": "5.16.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", + "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.6", - "@mui/system": "^5.16.6", + "@mui/core-downloads-tracker": "^5.16.7", + "@mui/system": "^5.16.7", "@mui/types": "^7.2.15", "@mui/utils": "^5.16.6", "@popperjs/core": "^2.11.8", @@ -5371,9 +5371,9 @@ } }, "node_modules/express-validator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.1.0.tgz", - "integrity": "sha512-ePn6NXjHRZiZkwTiU1Rl2hy6aUqmi6Cb4/s8sfUsKH7j2yYl9azSpl8xEHcOj1grzzQ+UBEoLWtE1s6FDxW++g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", + "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", diff --git a/package.json b/package.json index 83797bd..02d40a3 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@changey/react-leaflet-markercluster": "^4.0.0-rc1", "@emotion/react": "^11.13.0", "@emotion/styled": "^11.11.5", - "@mui/icons-material": "^5.16.5", - "@mui/material": "^5.16.6", + "@mui/icons-material": "^5.16.7", + "@mui/material": "^5.16.7", "@types/leaflet-rotatedmarker": "^0.2.5", "axios": "^1.7.4", "bcrypt": "^5.1.1", @@ -79,7 +79,7 @@ "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-slow-down": "^2.0.3", - "express-validator": "^7.1.0", + "express-validator": "^7.2.0", "helmet": "^7.1.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", diff --git a/src/models/entry.ts b/src/models/entry.ts index 799177b..e040c77 100644 --- a/src/models/entry.ts +++ b/src/models/entry.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from 'express'; import { checkExact, query } from 'express-validator'; -import { compare } from '@src/scripts/crypt'; import { create as createError } from '@src/middleware/error'; import * as file from '@src/scripts/file'; import { getTime } from '@src/scripts/time'; @@ -138,12 +137,12 @@ export function checkTime(value: string) { async function checkKey(value: string) { if (!value) { throw new Error('Key required'); } - if (!process.env.KEYB) { throw new Error('Configuration wrong'); } + if (!process.env.KEY) { throw new Error('Configuration wrong: KEY is missing in environment variables'); } if (process.env.NODE_ENV != "production" && value == "test") { return true; // dev testing convenience } - const result = await compare(decodeURIComponent(value), process.env.KEYB); + const result = Buffer.from(encodeURIComponent(value)).toString('base64') == process.env.KEY; if (!result) { throw new Error('Key does not match'); diff --git a/src/scripts/crypt.ts b/src/scripts/crypt.ts index 6bef0df..3ce03db 100644 --- a/src/scripts/crypt.ts +++ b/src/scripts/crypt.ts @@ -12,7 +12,7 @@ export const compare = async function (password: string, hash: string) { } function pepper(password: string) { - const key = process.env.KEYA; - if (!key) { throw new Error('KEYA is not defined in the environment variables'); } + const key = process.env.KEY; + if (!key) { throw new Error('KEY is not defined in the environment variables'); } return password + crypto.createHmac('sha256', key).digest("base64"); } diff --git a/src/scripts/token.ts b/src/scripts/token.ts index 9a012f2..4dc99ec 100644 --- a/src/scripts/token.ts +++ b/src/scripts/token.ts @@ -44,7 +44,7 @@ export function cleanupCSRF() { } export function validateJWT(req: Request) { - const key = process.env.KEYA; + const key = process.env.KEY; const header = req.header('Authorization'); const [type, token] = header ? header.split(' ') : ""; let payload: string | jwt.JwtPayload = ""; @@ -78,7 +78,7 @@ export function validateJWT(req: Request) { } export function createJWT(req: Request, res: Response) { - const key = process.env.KEYA; + const key = process.env.KEY; if (!key) { throw new Error('Configuration is wrong'); } const today = new Date(); const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index e7a593e..0a934bd 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -81,7 +81,6 @@ describe('Login', () => { it('test invalid credentials to return error', async () => { try { userDataWithToken.csrfToken = csrfToken; - console.log("csrfToken %o", userDataWithToken.csrfToken); await axios.post('http://localhost:80/login', qs.stringify(userDataWithToken)); } catch (error) { const axiosError = error as AxiosError;