Major bootstrap.
- Migrated frontend to Vue. - Migrated frontend map to OL API. - Extended environment variables. - README. - Country information/flag integration. - Implemented generic db/repo.
This commit is contained in:
parent
03deaa9cd8
commit
5dfde74ccf
40 changed files with 7309 additions and 223 deletions
.env.example.gitignoreapp.js
frontend
.editorconfig.gitattributes.prettierrc.jsonREADME.mdenv.d.tseslint.config.tsindex.htmlpackage-lock.jsonpackage.json
package-lock.jsonpackage.jsonpublic
src
tsconfig.app.jsontsconfig.jsontsconfig.node.jsonvite.config.tspublic
src
tsconfig.jsonviews
60
.env.example
60
.env.example
|
@ -1,2 +1,58 @@
|
|||
DATABASE_URL=postgres://user:password@host:port/dbname
|
||||
PORT=3000
|
||||
###
|
||||
### Backend configuration
|
||||
###
|
||||
|
||||
# Bind address for the backend (default: localhost)
|
||||
BACKEND_ADDRESS=127.0.0.1
|
||||
|
||||
# Listen port for the backend (default: 3000)
|
||||
BACKEND_PORT=3000
|
||||
|
||||
# Database URL (required)
|
||||
DB_URL=postgres://user:password@host:port/dbname
|
||||
|
||||
# Database dialect (default: inferred from the URL)
|
||||
# DB_DIALECT=postgres
|
||||
|
||||
# Name of the table that contains the location points (required)
|
||||
DB_LOCATION_TABLE=gpsdata
|
||||
|
||||
## Database mappings
|
||||
# The name of the column that contains the primary key of each location point
|
||||
DB_LOCATION__ID=id
|
||||
|
||||
# The name of the column that contains the timestamp of each location point
|
||||
DB_LOCATION__TIMESTAMP=timestamp
|
||||
|
||||
# The name of the column that contains the latitude of each location point
|
||||
DB_LOCATION__LATITUDE=latitude
|
||||
|
||||
# The name of the column that contains the longitude of each location point
|
||||
DB_LOCATION__LONGITUDE=longitude
|
||||
|
||||
# The name of the column that contains the altitude of each location point.
|
||||
# Comment or leave empty if the altitude is not available.
|
||||
DB_LOCATION__ALTITUDE=altitude
|
||||
|
||||
# The name of the column that contains the address of each location point.
|
||||
# Comment or leave empty if the address is not available.
|
||||
DB_LOCATION__ADDRESS=address
|
||||
|
||||
# The name of the column that contains the city/locality name of each location point
|
||||
# Comment or leave empty if the locality is not available.
|
||||
DB_LOCATION__LOCALITY=locality
|
||||
|
||||
# The name of the column that contains the country code of each location point
|
||||
# Comment or leave empty if the country code is not available.
|
||||
DB_LOCATION__COUNTRY=country
|
||||
|
||||
# The name of the column that contains the postal code of each location point
|
||||
# Comment or leave empty if the postal code is not available.
|
||||
DB_LOCATION__POSTAL_CODE=postal_code
|
||||
|
||||
###
|
||||
### Frontend configuration
|
||||
###
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:${BACKEND_PORT}
|
||||
VITE_API_PATH=/api
|
||||
|
|
27
.gitignore
vendored
27
.gitignore
vendored
|
@ -1,2 +1,27 @@
|
|||
node_modules
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
.env
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.vim
|
||||
*.tsbuildinfo
|
||||
|
|
58
app.js
58
app.js
|
@ -1,58 +0,0 @@
|
|||
const express = require('express');
|
||||
const dotenv = require('dotenv');
|
||||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// PostgreSQL connection using Sequelize
|
||||
const sequelize = new Sequelize(process.env.DATABASE_URL, {
|
||||
dialect: 'postgres',
|
||||
logging: false
|
||||
});
|
||||
|
||||
// Define GPS model
|
||||
const GpsData = sequelize.define('GpsData', {
|
||||
latitude: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false
|
||||
},
|
||||
longitude: {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
tableName: 'location_history',
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(express.static('public'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// View route
|
||||
app.get('/', async (req, res) => {
|
||||
res.render('index')
|
||||
});
|
||||
|
||||
// API route
|
||||
app.get('/gpsdata', async (req, res) => {
|
||||
const limit = req.query.limit || 100;
|
||||
const apiResponse = await GpsData.findAll({
|
||||
limit: limit,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
const gpsData = apiResponse.map((p) => p.dataValues);
|
||||
res.json(gpsData);
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
9
frontend/.editorconfig
Normal file
9
frontend/.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
1
frontend/.gitattributes
vendored
Normal file
1
frontend/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
7
frontend/.prettierrc.json
Normal file
7
frontend/.prettierrc.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
63
frontend/README.md
Normal file
63
frontend/README.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
# GPSTracker
|
||||
|
||||
##### Track your GPS data, from any data source
|
||||
|
||||
GPSTracker is a simple Webapp that consists of:
|
||||
|
||||
- A backend that:
|
||||
- Can read GPS data from any compatible data source (supported: `postgres`, `mysql`, `mariadb`, `mongodb`, `sqlite`,
|
||||
`snowflake`), with arbitrary complex filtering, and expose them over a simple Web API.
|
||||
- [[*TODO*]] Can ingest GPS data points from HTTP, MQTT, Websocket or Kafka.
|
||||
- A frontend to display GPS data points and provides advanced filtering.
|
||||
|
||||
## Building the application
|
||||
|
||||
```sh
|
||||
# Backend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See `.env.example` for a reference. Copy it to `.env` and modify it accordingly.
|
||||
|
||||
## Running the application
|
||||
|
||||
### Local installation
|
||||
|
||||
```sh
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
[[*TODO*]]
|
||||
|
||||
## Project Setup
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
#### Backend
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Frontend
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
2
frontend/env.d.ts
vendored
Normal file
2
frontend/env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="vite/client" />
|
||||
declare const __API_PATH__: string;
|
24
frontend/eslint.config.ts
Normal file
24
frontend/eslint.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'app/files-to-ignore',
|
||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormatting,
|
||||
)
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GPS Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
5659
frontend/package-lock.json
generated
Normal file
5659
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
frontend/package.json
Normal file
39
frontend/package.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "gpstracker",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"countries-list": "^3.1.1",
|
||||
"ol": "^10.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.4.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jiti": "^2.4.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"prettier": "^3.5.1",
|
||||
"sass-embedded": "^1.85.0",
|
||||
"typescript": "~5.7.3",
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vue-tsc": "^2.2.2"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: 32px | Height: 32px | Size: 4.2 KiB |
24
frontend/src/App.vue
Normal file
24
frontend/src/App.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<!--
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
-->
|
||||
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RouterLink,
|
||||
RouterView,
|
||||
},
|
||||
}
|
||||
</script>
|
86
frontend/src/assets/base.css
Normal file
86
frontend/src/assets/base.css
Normal file
|
@ -0,0 +1,86 @@
|
|||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After (image error) Size: 276 B |
31
frontend/src/assets/main.css
Normal file
31
frontend/src/assets/main.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
}
|
||||
}
|
171
frontend/src/components/Map.vue
Normal file
171
frontend/src/components/Map.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<main>
|
||||
<div class="loading" v-if="loading">Loading...</div>
|
||||
<div class="map-wrapper" v-else>
|
||||
<div id="map">
|
||||
<PointInfo :point="selectedPoint" ref="popup" @close="selectedPoint = null" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Feature from 'ol/Feature';
|
||||
import GPSPoint from '../models/GPSPoint';
|
||||
import Map from 'ol/Map';
|
||||
import OSM from 'ol/source/OSM';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import Point from 'ol/geom/Point';
|
||||
import PointInfo from './PointInfo.vue';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import View from 'ol/View';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import { Circle, Fill, Style } from 'ol/style';
|
||||
import { useGeographic } from 'ol/proj';
|
||||
import { type Nullable } from '../models/Types';
|
||||
|
||||
// @ts-ignore
|
||||
const baseURL = __API_PATH__
|
||||
useGeographic()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PointInfo,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
gpsPoints: [] as GPSPoint[],
|
||||
loading: false,
|
||||
map: null as Nullable<Map>,
|
||||
popup: null as Nullable<Overlay>,
|
||||
selectedPoint: null as Nullable<GPSPoint>,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(`${baseURL}/gpsdata`)
|
||||
return (await response.json())
|
||||
.map((gps: any) => {
|
||||
return new GPSPoint(gps)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
createMap(gpsPoints: GPSPoint[]) {
|
||||
const points = gpsPoints.map((gps: GPSPoint) => {
|
||||
const point = new Point([gps.longitude, gps.latitude])
|
||||
return point
|
||||
})
|
||||
|
||||
const pointFeatures = points.map((point: Point) => new Feature(point))
|
||||
const view = new View(this.getCenterAndZoom())
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM(),
|
||||
}),
|
||||
|
||||
new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: pointFeatures,
|
||||
}),
|
||||
style: new Style({
|
||||
image: new Circle({
|
||||
radius: 5,
|
||||
fill: new Fill({ color: 'red' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
view: view
|
||||
})
|
||||
|
||||
// @ts-expect-error
|
||||
this.$refs.popup.bindPopup(map)
|
||||
this.bindClick(map)
|
||||
return map
|
||||
},
|
||||
|
||||
getCenterAndZoom() {
|
||||
if (!this.gpsPoints?.length) {
|
||||
return {
|
||||
center: [0, 0],
|
||||
zoom: 2,
|
||||
}
|
||||
}
|
||||
|
||||
let [minX, minY, maxX, maxY] = [Infinity, Infinity, -Infinity, -Infinity]
|
||||
this.gpsPoints.forEach((gps: GPSPoint) => {
|
||||
minX = Math.min(minX, gps.longitude)
|
||||
minY = Math.min(minY, gps.latitude)
|
||||
maxX = Math.max(maxX, gps.longitude)
|
||||
maxY = Math.max(maxY, gps.latitude)
|
||||
})
|
||||
|
||||
const center = [(minX + maxX) / 2, (minY + maxY) / 2]
|
||||
const zoom = Math.max(2, Math.min(18, 18 - Math.log2(Math.max(maxX - minX, maxY - minY))))
|
||||
return { center, zoom }
|
||||
},
|
||||
|
||||
bindClick(map: Map) {
|
||||
map.on('click', (event) => {
|
||||
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => feature)
|
||||
if (feature) {
|
||||
const point = this.gpsPoints.find((gps: GPSPoint) => {
|
||||
const [longitude, latitude] = (feature.getGeometry() as any).getCoordinates()
|
||||
return gps.longitude === longitude && gps.latitude === latitude
|
||||
})
|
||||
|
||||
if (point) {
|
||||
this.selectedPoint = point
|
||||
// @ts-expect-error
|
||||
this.$refs.popup.setPosition(event.coordinate)
|
||||
}
|
||||
} else {
|
||||
this.selectedPoint = null
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.gpsPoints = await this.fetchData()
|
||||
this.map = this.createMap(this.gpsPoints)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "ol/ol.css";
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.ol-viewport) {
|
||||
.ol-attribution {
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
113
frontend/src/components/PointInfo.vue
Normal file
113
frontend/src/components/PointInfo.vue
Normal file
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="popup" :class="{ hidden: !point }" ref="popup">
|
||||
<div class="popup-content" v-if="point">
|
||||
<button @click="$emit('close')">Close</button>
|
||||
<div class="point-info">
|
||||
<h2 class="address" v-if="point.address">{{ point.address }}</h2>
|
||||
<h2 class="latlng" v-else>{{ point.latitude }}, {{ point.longitude }}</h2>
|
||||
<p class="latlng" v-if="point.address">{{ point.latitude }}, {{ point.longitude }}</p>
|
||||
<p class="locality" v-if="point.locality">{{ point.locality }}</p>
|
||||
<p class="postal-code" v-if="point.postalCode">{{ point.postalCode }}</p>
|
||||
<p class="country" v-if="country">
|
||||
<span class="flag" v-if="countryFlag">{{ countryFlag }} </span>
|
||||
<span class="name">{{ country.name }}</span>,
|
||||
<span class="continent">{{ country.continent }}</span>
|
||||
</p>
|
||||
<p class="timestamp" v-if="timeString">{{ timeString }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import GPSPoint from '../models/GPSPoint';
|
||||
import Map from 'ol/Map';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import type { TCountryCode } from 'countries-list';
|
||||
import { getCountryData, getEmojiFlag } from 'countries-list';
|
||||
|
||||
export default {
|
||||
emit: ['close'],
|
||||
props: {
|
||||
point: {
|
||||
type: [GPSPoint, null],
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
popup: null as Overlay | null,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
country() {
|
||||
const cc = this.point?.country as string | undefined
|
||||
if (cc?.length) {
|
||||
return getCountryData(cc.toUpperCase() as TCountryCode)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
countryFlag() {
|
||||
return this.country ? getEmojiFlag(this.country.iso2 as TCountryCode) : null
|
||||
},
|
||||
|
||||
timeString(): string | null {
|
||||
return this.point?.timestamp ? new Date(this.point.timestamp).toLocaleString() : null
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
bindPopup(map: Map) {
|
||||
this.popup = new Overlay({
|
||||
element: this.$refs.popup as HTMLElement,
|
||||
autoPan: true,
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
map.addOverlay(this.popup)
|
||||
},
|
||||
|
||||
setPosition(coordinates: number[]) {
|
||||
if (this.popup) {
|
||||
this.popup.setPosition(coordinates)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "ol/ol.css";
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
background: var(--color-background);
|
||||
min-width: 20em;
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
box-shadow: 2px 2px 2px 2px var(--color-border);
|
||||
|
||||
&.hidden {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
min-width: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
p.latlng {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
</style>
|
11
frontend/src/main.ts
Normal file
11
frontend/src/main.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
23
frontend/src/models/GPSPoint.ts
Normal file
23
frontend/src/models/GPSPoint.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
class GPSPoint {
|
||||
public latitude: number;
|
||||
public longitude: number;
|
||||
public altitude: number;
|
||||
public address: string;
|
||||
public locality: string;
|
||||
public country: string;
|
||||
public postalCode: string;
|
||||
public timestamp: Date;
|
||||
|
||||
constructor(public data: any) {
|
||||
this.latitude = data.latitude;
|
||||
this.longitude = data.longitude;
|
||||
this.altitude = data.altitude;
|
||||
this.address = data.address;
|
||||
this.locality = data.locality;
|
||||
this.country = data.country;
|
||||
this.postalCode = data.postalCode;
|
||||
this.timestamp = data.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
export default GPSPoint;
|
1
frontend/src/models/Types.ts
Normal file
1
frontend/src/models/Types.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type Nullable<T> = T | null;
|
23
frontend/src/router/index.ts
Normal file
23
frontend/src/router/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
//{
|
||||
// path: '/about',
|
||||
// name: 'about',
|
||||
// // route level code-splitting
|
||||
// // this generates a separate chunk (About.[hash].js) for this route
|
||||
// // which is lazy-loaded when the route is visited.
|
||||
// component: () => import('../views/AboutView.vue'),
|
||||
//},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
15
frontend/src/views/HomeView.vue
Normal file
15
frontend/src/views/HomeView.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import Map from '../components/Map.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Map,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<Map />
|
||||
</main>
|
||||
</template>
|
12
frontend/tsconfig.app.json
Normal file
12
frontend/tsconfig.app.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
frontend/tsconfig.json
Normal file
11
frontend/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
frontend/tsconfig.node.json
Normal file
19
frontend/tsconfig.node.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
44
frontend/vite.config.ts
Normal file
44
frontend/vite.config.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig((env) => {
|
||||
const envDir = '../';
|
||||
const envars = loadEnv(env.mode, envDir);
|
||||
const serverURL = new URL(
|
||||
envars.VITE_API_SERVER_URL ?? 'http://localhost:3000'
|
||||
);
|
||||
const serverAPIPath = envars.VITE_API_PATH ?? '/api';
|
||||
|
||||
return {
|
||||
envDir: envDir,
|
||||
|
||||
// make the API path globally available in the client
|
||||
define: {
|
||||
__API_PATH__: JSON.stringify(serverAPIPath),
|
||||
},
|
||||
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
// proxy requests with the API path to the server
|
||||
// <http://localhost:5173/api> -> <http://localhost:3000/api>
|
||||
[serverAPIPath]: serverURL.origin,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
557
package-lock.json
generated
557
package-lock.json
generated
|
@ -1,18 +1,57 @@
|
|||
{
|
||||
"name": "gps-tracker",
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gps-tracker",
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.0",
|
||||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"pg": "^8.7.1",
|
||||
"sequelize": "^6.6.5"
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"pg": "^8.13.3",
|
||||
"sequelize": "^6.37.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
|
@ -24,6 +63,46 @@
|
|||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
|
||||
"integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
|
||||
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
|
@ -39,6 +118,43 @@
|
|||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
|
||||
|
@ -58,19 +174,18 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
|
@ -79,18 +194,26 @@
|
|||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
|
@ -119,12 +242,26 @@
|
|||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
|
@ -163,44 +300,36 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
|
@ -239,6 +368,19 @@
|
|||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -305,21 +447,6 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
},
|
||||
"bin": {
|
||||
"ejs": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
|
@ -420,34 +547,17 @@
|
|||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
|
@ -486,6 +596,21 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
@ -532,6 +657,19 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
@ -545,12 +683,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
|
@ -605,6 +744,13 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inflection": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
|
||||
|
@ -629,22 +775,50 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/jake": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
||||
"license": "Apache-2.0",
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^3.2.3",
|
||||
"chalk": "^4.0.2",
|
||||
"filelist": "^1.0.4",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"jake": "bin/cli.js"
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
|
@ -726,6 +900,7 @@
|
|||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
|
@ -770,6 +945,79 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"nodemon": "bin/nodemon.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
@ -898,6 +1146,19 @@
|
|||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
|
@ -950,6 +1211,13 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
@ -989,6 +1257,19 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry-as-promised": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz",
|
||||
|
@ -1259,6 +1540,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
|
@ -1278,15 +1572,29 @@
|
|||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
|
@ -1304,6 +1612,16 @@
|
|||
"integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
@ -1317,6 +1635,27 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
|
|
42
package.json
42
package.json
|
@ -1,16 +1,40 @@
|
|||
{
|
||||
"name": "gps-tracker",
|
||||
"name": "server",
|
||||
"version": "1.0.0",
|
||||
"description": "A web interface to render GPS data points on a map",
|
||||
"main": "app.js",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "node app.js"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node dist/main.js",
|
||||
"dev": "nodemon",
|
||||
"build": "tsc"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"watch": [
|
||||
"src"
|
||||
],
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"exec": "tsc && node ./dist/main.js",
|
||||
"ext": "ts,js,json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.0",
|
||||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"pg": "^8.7.1",
|
||||
"sequelize": "^6.6.5"
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"pg": "^8.13.3",
|
||||
"sequelize": "^6.37.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
#map {
|
||||
height: 100vh;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
async function getGpsData() {
|
||||
return await fetch('/gpsdata')
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const map = L.map('map').setView([0, 0], 2);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
getGpsData().then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Response status: ${response.status}`);
|
||||
}
|
||||
|
||||
response.json().then((gpsData) => {
|
||||
console.log(gpsData);
|
||||
gpsData.forEach(point => {
|
||||
L.marker([point.latitude, point.longitude]).addTo(map)
|
||||
.bindPopup(`Latitude: ${point.latitude}, Longitude: ${point.longitude}`)
|
||||
.openPopup();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
158
src/db/Db.ts
Normal file
158
src/db/Db.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
import { Sequelize, DataTypes, Dialect } from 'sequelize';
|
||||
|
||||
export class Db {
|
||||
private readonly url: string;
|
||||
private readonly locationTable: string;
|
||||
public readonly locationTableColumns: object;
|
||||
private readonly dialect: Dialect;
|
||||
private readonly sequelize: Sequelize;
|
||||
|
||||
private static readonly envColumnPrefix = 'DB_LOCATION__';
|
||||
|
||||
private constructor(
|
||||
opts: {
|
||||
url: string,
|
||||
locationTable: string,
|
||||
locationTableColumns: object,
|
||||
dialect: Dialect,
|
||||
}
|
||||
) {
|
||||
this.url = opts.url;
|
||||
this.locationTable = opts.locationTable;
|
||||
this.locationTableColumns = opts.locationTableColumns
|
||||
this.dialect = opts.dialect as Dialect;
|
||||
|
||||
this.sequelize = new Sequelize(this.url, {
|
||||
dialect: this.dialect,
|
||||
logging: process.env.DEBUG === 'true' ? console.log : false
|
||||
});
|
||||
}
|
||||
|
||||
public static fromEnv(): Db {
|
||||
const opts: any = {}
|
||||
opts.url = process.env.DB_URL;
|
||||
opts.locationTable = process.env.DB_LOCATION_TABLE;
|
||||
opts.dialect = process.env.DB_DIALECT || opts.url.split(':')[0];
|
||||
|
||||
if (!opts.url?.length) {
|
||||
console.error('No DB_URL provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!opts.locationTable?.length) {
|
||||
console.error('No LOCATION_TABLE provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const requiredColumns = ['id', 'timestamp', 'latitude', 'longitude']
|
||||
.reduce((acc: any, name: string) => {
|
||||
acc[name] = true
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
opts.locationTableColumns = [
|
||||
'id',
|
||||
'timestamp',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'altitude',
|
||||
'address',
|
||||
'locality',
|
||||
'country',
|
||||
'postal_code'
|
||||
].reduce((acc: any, name: string) => {
|
||||
acc[name] = process.env[this.prefixed(name)];
|
||||
if (!acc[name]?.length && requiredColumns[name]) {
|
||||
// Default to the name of the required field
|
||||
acc[name] = name;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return new Db(opts);
|
||||
}
|
||||
|
||||
private static prefixed(name: string): string {
|
||||
return `${Db.envColumnPrefix}${name.toUpperCase()}`;
|
||||
}
|
||||
|
||||
public GpsData() {
|
||||
const typeDef: any = {};
|
||||
|
||||
// @ts-expect-error
|
||||
typeDef[this.locationTableColumns['id']] = {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
};
|
||||
|
||||
// @ts-expect-error
|
||||
typeDef[this.locationTableColumns['latitude']] = {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false
|
||||
};
|
||||
|
||||
// @ts-expect-error
|
||||
typeDef[this.locationTableColumns['longitude']] = {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: false
|
||||
};
|
||||
|
||||
// @ts-expect-error
|
||||
const altitudeCol: string = this.locationTableColumns['altitude'];
|
||||
if (altitudeCol?.length) {
|
||||
typeDef[altitudeCol] = {
|
||||
type: DataTypes.FLOAT,
|
||||
allowNull: true
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const addressCol: string = this.locationTableColumns['address'];
|
||||
if (addressCol?.length) {
|
||||
typeDef[addressCol] = {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const localityCol: string = this.locationTableColumns['locality'];
|
||||
if (localityCol?.length) {
|
||||
typeDef[localityCol] = {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const countryCol: string = this.locationTableColumns['country'];
|
||||
if (countryCol?.length) {
|
||||
typeDef[countryCol] = {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const postalCodeCol: string = this.locationTableColumns['postal_code'];
|
||||
if (postalCodeCol?.length) {
|
||||
typeDef[postalCodeCol] = {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
typeDef[this.locationTableColumns['timestamp']] = {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
};
|
||||
|
||||
return this.sequelize.define('GpsData', typeDef, {
|
||||
tableName: this.locationTable,
|
||||
timestamps: false
|
||||
});
|
||||
}
|
||||
}
|
47
src/main.ts
Normal file
47
src/main.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import { Db } from './db/Db';
|
||||
import { LocationRequest } from './models/LocationRequest';
|
||||
import { LocationRepository } from './repo/LocationRepository';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const db = Db.fromEnv();
|
||||
const address = process.env.BACKEND_ADDRESS || '127.0.0.1';
|
||||
const port = process.env.BACKEND_PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.static('frontend/dist'));
|
||||
|
||||
// API route
|
||||
app.get('/api/gpsdata', async (req, res) => {
|
||||
console.log(`[${req.ip}] GET ${req.originalUrl}`);
|
||||
let query: any = {};
|
||||
|
||||
try {
|
||||
query = new LocationRequest(req.query);
|
||||
} catch (error) {
|
||||
const e = `Error parsing query: ${error}`;
|
||||
console.warn(e);
|
||||
res.status(400).send(e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const gpsData = await new LocationRepository(db).getHistory(query);
|
||||
res.json(gpsData);
|
||||
} catch (error) {
|
||||
const e = `Error fetching data: ${error}`;
|
||||
console.error(e);
|
||||
res.status(500).send(e);
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
app.listen(port, address, () => {
|
||||
console.log(`Server is running on port ${address}:${port}`);
|
||||
});
|
25
src/models/GPSPoint.ts
Normal file
25
src/models/GPSPoint.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
class GPSPoint {
|
||||
public id: number;
|
||||
public latitude: number;
|
||||
public longitude: number;
|
||||
public altitude: number | null;
|
||||
public address: string | null;
|
||||
public locality: string | null;
|
||||
public country: string | null;
|
||||
public postalCode: string | null;
|
||||
public timestamp: Date;
|
||||
|
||||
constructor(record: any) {
|
||||
this.id = record.id;
|
||||
this.latitude = record.latitude;
|
||||
this.longitude = record.longitude;
|
||||
this.altitude = record.altitude;
|
||||
this.address = record.address;
|
||||
this.locality = record.locality;
|
||||
this.country = record.country;
|
||||
this.postalCode = record.postalCode;
|
||||
this.timestamp = record.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
export { GPSPoint };
|
39
src/models/LocationRequest.ts
Normal file
39
src/models/LocationRequest.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Nullable } from './Types';
|
||||
|
||||
class LocationRequest {
|
||||
limit: Nullable<number> = 10;
|
||||
offset: Nullable<number> = null;
|
||||
|
||||
constructor(req: any) {
|
||||
if (req.limit != null) {
|
||||
this.limit = parseInt(req.limit);
|
||||
if (isNaN(this.limit)) {
|
||||
throw new TypeError('Invalid limit');
|
||||
}
|
||||
}
|
||||
|
||||
if (req.offset != null) {
|
||||
this.offset = parseInt(req.offset);
|
||||
if (isNaN(this.offset)) {
|
||||
throw new TypeError('Invalid offset');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public toMap(): any {
|
||||
let map: any = {};
|
||||
|
||||
if (this.limit != null) {
|
||||
map.limit = this.limit;
|
||||
}
|
||||
|
||||
if (this.offset != null) {
|
||||
map.offset = this.offset;
|
||||
}
|
||||
|
||||
map.order = [['created_at', 'DESC']];
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
export { LocationRequest };
|
3
src/models/Types.ts
Normal file
3
src/models/Types.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
type Nullable<T> = T | null;
|
||||
|
||||
export { Nullable };
|
42
src/repo/LocationRepository.ts
Normal file
42
src/repo/LocationRepository.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Db } from '../db/Db';
|
||||
import { GPSPoint } from '../models/GPSPoint';
|
||||
import { LocationRequest } from 'src/models/LocationRequest';
|
||||
|
||||
export class LocationRepository {
|
||||
private db: Db;
|
||||
|
||||
constructor(db: Db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public async getHistory(query: LocationRequest): Promise<GPSPoint[]> {
|
||||
let apiResponse: any[] = [];
|
||||
|
||||
try {
|
||||
apiResponse = await this.db.GpsData().findAll(query.toMap());
|
||||
} catch (error) {
|
||||
throw new Error(`Error fetching data: ${error}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return apiResponse.map((p) => {
|
||||
const data = p.dataValues;
|
||||
const mappings: any = this.db.locationTableColumns;
|
||||
|
||||
return new GPSPoint({
|
||||
id: data[mappings.id],
|
||||
latitude: data[mappings.latitude],
|
||||
longitude: data[mappings.longitude],
|
||||
altitude: data[mappings.altitude],
|
||||
address: data[mappings.address],
|
||||
locality: data[mappings.locality],
|
||||
country: data[mappings.country],
|
||||
postalCode: data[mappings.postal_code],
|
||||
timestamp: data[mappings.timestamp],
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Error parsing data: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node",
|
||||
"removeComments": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GPS Tracker</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue