commit b0ee86c9247f09588422a708ffdb8900ec828b04 Author: Miksa Date: Fri May 22 23:05:37 2026 +0200 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..09e2187 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +.git +.gitignore +.env +uploads diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb1c38d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +PORT=3000 +JWT_SECRET=elevator-manager-secret +DATABASE_URL=postgresql://lift_admin:lift_secret@127.0.0.1:5432/lift_manager diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e77f51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +uploads/ +data/store.json +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..e35dc32 --- /dev/null +++ b/README.md @@ -0,0 +1,239 @@ +# LiftEllenőr + +Magyar nyelvű liftellenőrzési és naplózási rendszer technikusok számára. + +Az alkalmazás főbb részei: + +- nyilvános landing oldal: `/` +- bejelentkezés: `/bejelentkezes` +- regisztráció: `/regisztracio` +- technikusi kezelőfelület: `/alkalmazas` +- kereshető naplóoldal: `/naplo` + +## Funkciók + +- JWT alapú hitelesítés +- PostgreSQL adatbázis +- külön kezelt eszközök és naplóbejegyzések +- modalos új eszköz / új napló folyamat +- PDF csatolmányok feltöltése egy naplóhoz +- közelgő ellenőrzési határidők műszerfala +- Adminer webes adatbázis-kezelő a stack részeként +- nagy mennyiségű tesztadat generálása seed scripttel + +## Stack + +A `compose.yaml` alapján a rendszer ezekből a szolgáltatásokból áll: + +- `frontend` + Nginx konténer, a statikus frontend kiszolgálására és az API proxyzására. +- `backend` + Node.js + Express alkalmazás, API-val és hitelesítéssel. +- `postgres` + PostgreSQL adatbázis a felhasználók, eszközök, naplók és fájlmetaadatok tárolására. +- `adminer` + Webes adatbázis-kezelő a PostgreSQL gyors böngészéséhez és ellenőrzéséhez. + +## Portok + +- alkalmazás: `http://localhost:8080` +- PostgreSQL: `localhost:5432` +- Adminer: `http://localhost:8090` + +## Előfeltételek + +- Node.js 22 körüli verzió +- npm +- Podman Compose vagy Docker Compose + +## Gyors indítás + +1. Függőségek telepítése: + +```bash +npm install +``` + +2. A teljes stack indítása: + +```bash +podman compose up -d +``` + +Ha Docker Compose-t használsz: + +```bash +docker compose up -d +``` + +3. Nyisd meg az alkalmazást: + +```text +http://localhost:8080 +``` + +## Stack kezelése + +Indítás: + +```bash +podman compose up -d +``` + +Leállítás: + +```bash +podman compose down +``` + +Leállítás kötetekkel együtt: + +```bash +podman compose down -v +``` + +Logok megtekintése: + +```bash +podman compose logs -f +``` + +Csak a backend logjai: + +```bash +podman compose logs -f backend +``` + +## Adminer használata + +Az Adminer a `http://localhost:8090` címen érhető el. + +Bejelentkezési adatok: + +- rendszer: `PostgreSQL` +- szerver: `postgres` + Ha a böngészőből közvetlenül a hoston futó PostgreSQL-re csatlakozol, használhatod a `127.0.0.1` címet is, de a compose stacken belül a szolgáltatás neve `postgres`. +- felhasználó: `lift_admin` +- jelszó: `lift_secret` +- adatbázis: `lift_manager` + +Adminerben hasznos műveletek: + +- `users` tábla ellenőrzése +- `elevators` tábla böngészése +- `logs` és `log_files` kapcsolatok megtekintése +- seed után gyors ellenőrzés, hogy a tömeges tesztadat bekerült-e + +## Seed adatok + +A projekt tartalmaz egy seed scriptet sok tesztadat előállításához: + +```bash +npm run seed +``` + +Ez több útvonalat, technikust, eszközt és sok kapcsolódó naplóbejegyzést hoz létre. + +Alap teszt jelszó: + +```text +teszt123 +``` + +Teljes újratöltéshez, a meglévő adatok törlésével: + +```bash +npm run seed:reset +``` + +Egyedi mennyiségekkel: + +```bash +node scripts/seed.js --reset --routes 8 --users-per-route 4 --elevators-per-route 20 --logs-per-elevator 16 +``` + +Elérhető paraméterek: + +- `--reset` + Törli a meglévő adatokat seed előtt. +- `--routes` + Hány technikusi útvonal készüljön. +- `--users-per-route` + Hány felhasználó készüljön útvonalanként. +- `--elevators-per-route` + Hány eszköz készüljön útvonalanként. +- `--logs-per-elevator` + Hány napló készüljön eszközönként. + +## Fejlesztői használat + +Ha nem konténerből akarod indítani a backendet: + +1. gondoskodj róla, hogy a PostgreSQL fusson +2. telepítsd a függőségeket +3. indítsd a szervert + +```bash +npm install +npm start +``` + +Alapértelmezett backend port: + +```text +http://localhost:3000 +``` + +Ebben az esetben a frontend proxyzása nélkül a teljes alkalmazás használatához továbbra is a compose-os Nginx a legegyszerűbb út. + +## Környezeti változók + +Az alapértelmezett értékek az `.env.example` fájlban vannak összegyűjtve. + +Fontos változók: + +- `PORT` +- `JWT_SECRET` +- `DATABASE_URL` +- `SEED_PASSWORD` + Opcionális. Ezzel felülírható a seedelt felhasználók alap jelszava. + +## Adatmodell röviden + +- `users` + Technikusi felhasználók és útvonalak. +- `elevators` + Az eszközök aktuális állapota és következő ellenőrzési adatai. +- `logs` + Történeti naplóbejegyzések, eszközhöz kapcsolva. +- `log_files` + A naplóbejegyzésekhez tartozó PDF fájlmetaadatok. + +## Tipikus használati folyamat + +1. Indítsd el a stack-et. +2. Ha kell, seedeld az adatbázist `npm run seed:reset` paranccsal. +3. Nyisd meg az alkalmazást a `http://localhost:8080` címen. +4. Jelentkezz be vagy regisztrálj. +5. Az `/alkalmazas` oldalon kezeld az eszközöket és hozz létre új naplóbejegyzéseket. +6. A `/naplo` oldalon keresd és szűrd a bejegyzéseket. +7. Adminerben ellenőrizd az adatokat, ha fejlesztés vagy hibakeresés közben szükséges. + +## Fontos fájlok + +- `server.js` + Express route-ok, auth, adatbázis-műveletek +- `compose.yaml` + frontend + backend + postgres + adminer stack +- `nginx/default.conf` + frontend route-ok és API proxy +- `public/index.html` + landing oldal +- `public/app.html` + kezelőfelület +- `public/logs.html` + külön naplóoldal +- `public/style.css` + központi stíluslap +- `scripts/seed.js` + tesztadat-generáló script diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4dd2ba9 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,54 @@ +services: + frontend: + image: docker.io/library/nginx:1.27-alpine + container_name: lift-frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "8080:80" + volumes: + - ./public:/usr/share/nginx/html:ro + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + + backend: + image: docker.io/library/node:22-alpine + container_name: lift-backend + restart: unless-stopped + depends_on: + - postgres + working_dir: /app + environment: + PORT: 3000 + JWT_SECRET: elevator-manager-secret + DATABASE_URL: postgresql://lift_admin:lift_secret@postgres:5432/lift_manager + command: sh -c "npm ci --omit=dev && npm start" + volumes: + - ./package.json:/app/package.json:ro + - ./package-lock.json:/app/package-lock.json:ro + - ./server.js:/app/server.js:ro + - ./public:/app/public:ro + - ./data:/app/data + - ./uploads:/app/uploads + + postgres: + image: docker.io/library/postgres:16-alpine + container_name: lift-postgres + restart: unless-stopped + environment: + POSTGRES_DB: lift_manager + POSTGRES_USER: lift_admin + POSTGRES_PASSWORD: lift_secret + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + adminer: + image: adminer + restart: always + ports: + - 8090:8080 + +volumes: + postgres_data: diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..1eb960b --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,67 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index login.html; + + location = / { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /bejelentkezes { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /regisztracio { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /alkalmazas { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /naplo { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/ { + proxy_pass http://backend:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate"; + expires off; + try_files $uri $uri/ =404; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..39efa8f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1785 @@ +{ + "name": "elevator-man", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "elevator-man", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.13.1" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "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==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.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", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0cac808 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "elevator-man", + "version": "1.0.0", + "description": "Magyar nyelvű liftellenőrzési kezelő JWT auth-tal, PDF feltöltéssel és PostgreSQL adatbázissal.", + "main": "server.js", + "scripts": { + "start": "node server.js", + "seed": "node scripts/seed.js", + "seed:reset": "node scripts/seed.js --reset" + }, + "keywords": [ + "elevator", + "inspection", + "jwt", + "bcrypt", + "postgres" + ], + "author": "", + "license": "ISC", + "dependencies": { + "bcrypt": "^5.1.1", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.13.1" + } +} diff --git a/public/app.html b/public/app.html new file mode 100644 index 0000000..277fe59 --- /dev/null +++ b/public/app.html @@ -0,0 +1,189 @@ + + + + + + Alkalmazás | LiftEllenőr + + + +
+
+
+
+ +
+

Kezelőfelület

+

+
+
+
+ Napló megnyitása + +
+
+ +
+
+
+

Határidők

+

A bejelentkezett technikus útvonalához tartozó, a következő 7 napban esedékes ellenőrzések.

+
+
+
+
+ +
+
+
+

Eszközök

+

Külön kezelt eszközlista, amelynek az állapota új naplóbejegyzésekkel frissül.

+
+
+ + +
+
+
+
+
+
+ + + + + + + + diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..257d6f2 --- /dev/null +++ b/public/app.js @@ -0,0 +1,543 @@ +const state = { + user: null, + devices: [], + dashboardElevators: [], + countdownIntervalId: null, + activeModal: null, + selectedLogElevatorId: null +}; + +const elements = { + welcomeText: document.getElementById("welcomeText"), + routeText: document.getElementById("routeText"), + logoutButton: document.getElementById("logoutButton"), + dashboardCards: document.getElementById("dashboardCards"), + deviceList: document.getElementById("deviceList"), + openDeviceModalButton: document.getElementById("openDeviceModalButton"), + openLogModalButton: document.getElementById("openLogModalButton"), + deviceModal: document.getElementById("deviceModal"), + logModal: document.getElementById("logModal"), + deviceForm: document.getElementById("deviceForm"), + deviceEditId: document.getElementById("deviceEditId"), + deviceModalTitle: document.getElementById("deviceModalTitle"), + deviceModalSubtitle: document.getElementById("deviceModalSubtitle"), + deviceRouteName: document.getElementById("deviceRouteName"), + deviceName: document.getElementById("deviceName"), + deviceSerialNumber: document.getElementById("deviceSerialNumber"), + deviceLocation: document.getElementById("deviceLocation"), + deviceStatus: document.getElementById("deviceStatus"), + deviceInspectionIntervalDays: document.getElementById("deviceInspectionIntervalDays"), + deviceNextInspectionDate: document.getElementById("deviceNextInspectionDate"), + deviceMessage: document.getElementById("deviceMessage"), + closeDeviceModalButton: document.getElementById("closeDeviceModalButton"), + cancelDeviceModalButton: document.getElementById("cancelDeviceModalButton"), + logForm: document.getElementById("logForm"), + logElevatorId: document.getElementById("logElevatorId"), + logElevatorSummary: document.getElementById("logElevatorSummary"), + logStatus: document.getElementById("logStatus"), + logInspectionDate: document.getElementById("logInspectionDate"), + logInspectionIntervalDays: document.getElementById("logInspectionIntervalDays"), + logNextInspectionDate: document.getElementById("logNextInspectionDate"), + logMessage: document.getElementById("logMessage"), + closeLogModalButton: document.getElementById("closeLogModalButton"), + cancelLogModalButton: document.getElementById("cancelLogModalButton"), + openDeviceFromLogButton: document.getElementById("openDeviceFromLogButton") +}; + +bindEvents(); +bootPage(); + +function bindEvents() { + elements.logoutButton.addEventListener("click", logout); + elements.openDeviceModalButton.addEventListener("click", () => openDeviceModal()); + elements.openLogModalButton.addEventListener("click", () => openLogModal()); + elements.deviceForm.addEventListener("submit", handleDeviceSave); + elements.logForm.addEventListener("submit", handleLogSave); + elements.closeDeviceModalButton.addEventListener("click", closeActiveModal); + elements.cancelDeviceModalButton.addEventListener("click", closeActiveModal); + elements.closeLogModalButton.addEventListener("click", closeActiveModal); + elements.cancelLogModalButton.addEventListener("click", closeActiveModal); + elements.openDeviceFromLogButton.addEventListener("click", handleOpenDeviceFromLog); + elements.logElevatorId.addEventListener("change", handleLogElevatorChange); + elements.logInspectionDate.addEventListener("change", syncLogNextInspectionDate); + elements.logInspectionIntervalDays.addEventListener("input", syncLogNextInspectionDate); + elements.deviceList.addEventListener("click", handleDeviceListClick); + elements.deviceModal.addEventListener("click", handleModalBackdropClick); + elements.logModal.addEventListener("click", handleModalBackdropClick); + window.addEventListener("keydown", handleWindowKeydown); + window.addEventListener("pageshow", handlePageShow); +} + +async function bootPage() { + setToday(elements.logInspectionDate); + syncLogNextInspectionDate(); + renderDevices(); + renderDashboard(); + + try { + const response = await apiFetch("/api/me"); + state.user = response.user; + fillUserHeader(); + elements.deviceRouteName.value = state.user?.routeName || ""; + await Promise.allSettled([loadDashboard(), loadDevices()]); + } catch (error) { + if (error.status === 401) { + window.location.href = "/bejelentkezes"; + return; + } + + elements.welcomeText.textContent = "Betöltési hiba"; + elements.routeText.textContent = "A munkamenet ellenőrzése nem sikerült. Frissítsd az oldalt vagy jelentkezz be újra."; + setFeedback(elements.logMessage, error.message, "error"); + } +} + +function fillUserHeader() { + const name = state.user?.fullName || "Felhasználó"; + const route = state.user?.routeName || "Nincs megadva"; + elements.welcomeText.textContent = "Kezelőfelület"; + elements.routeText.textContent = `${name} · Technikusi útvonal: ${route}`; +} + +async function loadDashboard() { + try { + const response = await apiFetch("/api/dashboard"); + state.dashboardElevators = response.elevators || []; + renderDashboard(); + } catch (error) { + state.dashboardElevators = []; + renderDashboard(); + setFeedback(elements.logMessage, error.message, "error"); + } +} + +async function loadDevices() { + try { + const response = await apiFetch("/api/elevators"); + state.devices = response.elevators || []; + renderDevices(); + renderLogElevatorOptions(state.selectedLogElevatorId); + } catch (error) { + state.devices = []; + renderDevices(); + renderLogElevatorOptions(null); + setFeedback(elements.deviceMessage, error.message, "error"); + } +} + +function renderDashboard() { + if (!state.dashboardElevators.length) { + elements.dashboardCards.innerHTML = ` +
+

Nincs közelgő határidő

+

A következő 7 napban esedékes ellenőrzések itt jelennek meg.

+
+ `; + clearCountdownTimer(); + return; + } + + elements.dashboardCards.innerHTML = state.dashboardElevators + .map((elevator) => { + const countdown = calculateCountdown(elevator.nextInspectionDate); + const statusClass = countdown.expired ? "danger-chip" : countdown.warning ? "warning-chip" : "success-chip"; + + return ` +
+
+ ${countdown.label} + ${escapeHtml(elevator.serialNumber)} +
+

${escapeHtml(elevator.name)}

+

${escapeHtml(elevator.location)}

+

Legutóbbi állapot: ${humanizeStatus(elevator.lastStatus)}

+
+ ${countdown.text} +
+

Következő ellenőrzés: ${formatDate(elevator.nextInspectionDate)}

+
+ `; + }) + .join(""); + + clearCountdownTimer(); + state.countdownIntervalId = window.setInterval(updateCountdownBoxes, 1000); +} + +function renderDevices() { + if (!state.devices.length) { + elements.deviceList.innerHTML = ` +
+

Még nincs rögzített eszköz

+

Hozz létre egy új eszközt, majd adj hozzá naplóbejegyzéseket az állapot frissítéséhez.

+
+ `; + elements.openLogModalButton.disabled = true; + return; + } + + elements.openLogModalButton.disabled = false; + elements.deviceList.innerHTML = state.devices + .map((device) => ` +
+
+ ${humanizeStatus(device.lastStatus)} + ${escapeHtml(device.serialNumber)} +
+

${escapeHtml(device.name)}

+

Helyszín: ${escapeHtml(device.location)}

+

Útvonal: ${escapeHtml(device.routeName)}

+

Utolsó ellenőrzés: ${formatDate(device.lastInspectionDate)}

+

Következő ellenőrzés: ${formatDate(device.nextInspectionDate)}

+

Ciklus: ${escapeHtml(device.inspectionIntervalDays)} nap

+
+ + +
+
+ `) + .join(""); +} + +function renderLogElevatorOptions(preferredElevatorId) { + if (!state.devices.length) { + state.selectedLogElevatorId = null; + elements.logElevatorId.innerHTML = ``; + elements.logElevatorSummary.textContent = "A naplózáshoz előbb egy eszközt kell létrehozni."; + return; + } + + const fallbackId = preferredElevatorId || state.selectedLogElevatorId || state.devices[0].id; + const resolvedId = state.devices.some((device) => device.id === Number(fallbackId)) ? Number(fallbackId) : state.devices[0].id; + state.selectedLogElevatorId = resolvedId; + + elements.logElevatorId.innerHTML = state.devices + .map( + (device) => ` + + ` + ) + .join(""); + + renderSelectedElevatorSummary(resolvedId); +} + +function renderSelectedElevatorSummary(elevatorId) { + const device = state.devices.find((entry) => entry.id === Number(elevatorId)); + + if (!device) { + elements.logElevatorSummary.textContent = "Válassz ki egy eszközt a naplóbejegyzéshez."; + return; + } + + elements.logElevatorSummary.innerHTML = ` + ${escapeHtml(device.name)}
+ Gyári szám: ${escapeHtml(device.serialNumber)}
+ Helyszín: ${escapeHtml(device.location)}
+ Utolsó állapot: ${humanizeStatus(device.lastStatus)}
+ Következő ellenőrzés: ${formatDate(device.nextInspectionDate)} + `; +} + +function updateCountdownBoxes() { + document.querySelectorAll(".countdown-box").forEach((box) => { + const countdown = calculateCountdown(box.dataset.nextDate); + box.textContent = countdown.text; + }); +} + +function clearCountdownTimer() { + if (state.countdownIntervalId) { + window.clearInterval(state.countdownIntervalId); + state.countdownIntervalId = null; + } +} + +function handlePageShow() { + if (state.user) { + Promise.allSettled([loadDashboard(), loadDevices()]); + } +} + +function handleDeviceListClick(event) { + const button = event.target.closest("button[data-action]"); + + if (!button) { + return; + } + + const elevatorId = Number(button.dataset.elevatorId); + + if (button.dataset.action === "log") { + openLogModal(elevatorId); + return; + } + + if (button.dataset.action === "edit") { + openDeviceModal(elevatorId); + } +} + +function handleLogElevatorChange() { + const elevatorId = Number(elements.logElevatorId.value); + state.selectedLogElevatorId = Number.isInteger(elevatorId) && elevatorId > 0 ? elevatorId : null; + renderSelectedElevatorSummary(state.selectedLogElevatorId); + + const device = getSelectedElevator(); + + if (device) { + elements.logInspectionIntervalDays.value = device.inspectionIntervalDays || 30; + syncLogNextInspectionDate(); + } +} + +function handleModalBackdropClick(event) { + if (event.target === event.currentTarget) { + closeActiveModal(); + } +} + +function handleWindowKeydown(event) { + if (event.key === "Escape" && state.activeModal) { + closeActiveModal(); + } +} + +function handleOpenDeviceFromLog() { + closeActiveModal(); + openDeviceModal(null, true); +} + +async function handleDeviceSave(event) { + event.preventDefault(); + + const elevatorId = Number(elements.deviceEditId.value || 0); + const payload = { + name: elements.deviceName.value, + serialNumber: elements.deviceSerialNumber.value, + location: elements.deviceLocation.value, + routeName: elements.deviceRouteName.value, + status: elements.deviceStatus.value, + inspectionIntervalDays: elements.deviceInspectionIntervalDays.value, + nextInspectionDate: elements.deviceNextInspectionDate.value || "", + lastInspectionDate: elements.deviceForm.dataset.lastInspectionDate || "" + }; + + try { + const response = await apiFetch(elevatorId ? `/api/elevators/${elevatorId}` : "/api/elevators", { + method: elevatorId ? "PUT" : "POST", + body: JSON.stringify(payload) + }); + + setFeedback(elements.deviceMessage, response.message, "success"); + await Promise.allSettled([loadDevices(), loadDashboard()]); + closeActiveModal(); + + if (elements.deviceForm.dataset.returnToLog === "true" && response.elevator?.id) { + openLogModal(response.elevator.id); + } + } catch (error) { + setFeedback(elements.deviceMessage, error.message, "error"); + } +} + +async function handleLogSave(event) { + event.preventDefault(); + + const formData = new FormData(elements.logForm); + + try { + const response = await apiFetch("/api/logs", { + method: "POST", + body: formData, + isFormData: true + }); + + setFeedback(elements.logMessage, response.message, "success"); + await Promise.allSettled([loadDashboard(), loadDevices()]); + closeActiveModal(); + } catch (error) { + setFeedback(elements.logMessage, error.message, "error"); + } +} + +async function logout() { + try { + const response = await apiFetch("/api/logout", { method: "POST" }); + window.location.href = response.redirectPath; + } catch (_error) { + window.location.href = "/bejelentkezes"; + } +} + +function openDeviceModal(elevatorId = null, returnToLog = false) { + const device = state.devices.find((entry) => entry.id === Number(elevatorId)); + elements.deviceForm.reset(); + elements.deviceForm.dataset.returnToLog = returnToLog ? "true" : "false"; + elements.deviceForm.dataset.lastInspectionDate = normalizeDateInput(device?.lastInspectionDate); + elements.deviceEditId.value = device?.id || ""; + elements.deviceRouteName.value = state.user?.routeName || ""; + elements.deviceInspectionIntervalDays.value = device?.inspectionIntervalDays || 30; + elements.deviceStatus.value = device?.lastStatus || "rendben"; + elements.deviceNextInspectionDate.value = normalizeDateInput(device?.nextInspectionDate); + elements.deviceName.value = device?.name || ""; + elements.deviceSerialNumber.value = device?.serialNumber || ""; + elements.deviceLocation.value = device?.location || ""; + elements.deviceModalTitle.textContent = device ? "Eszköz szerkesztése" : "Új eszköz"; + elements.deviceModalSubtitle.textContent = device + ? "Az eszköz alapadatainak frissítése. Az állapotot naplóbejegyzéssel is módosíthatod." + : "Új lift vagy berendezés rögzítése a technikusi útvonalhoz."; + setFeedback(elements.deviceMessage, "", ""); + showModal(elements.deviceModal); +} + +function openLogModal(elevatorId = null) { + if (!state.devices.length) { + openDeviceModal(); + setFeedback(elements.deviceMessage, "A naplózáshoz előbb létre kell hoznod egy eszközt.", "error"); + return; + } + + elements.logForm.reset(); + setFeedback(elements.logMessage, "", ""); + setToday(elements.logInspectionDate); + elements.logStatus.value = "rendben"; + renderLogElevatorOptions(elevatorId); + elements.logElevatorId.value = String(state.selectedLogElevatorId || ""); + handleLogElevatorChange(); + showModal(elements.logModal); +} + +function showModal(modalElement) { + closeActiveModal(); + state.activeModal = modalElement; + modalElement.classList.remove("hidden"); + document.body.classList.add("modal-open"); +} + +function closeActiveModal() { + if (!state.activeModal) { + return; + } + + state.activeModal.classList.add("hidden"); + state.activeModal = null; + document.body.classList.remove("modal-open"); +} + +function getSelectedElevator() { + return state.devices.find((device) => device.id === Number(state.selectedLogElevatorId)) || null; +} + +async function apiFetch(url, options = {}) { + const headers = options.isFormData ? {} : { "Content-Type": "application/json" }; + const response = await fetch(url, { + method: options.method || "GET", + credentials: "include", + headers, + body: options.body + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : { message: "A szerver nem JSON választ adott vissza." }; + + if (!response.ok) { + const error = new Error(data.message || "A kérés nem sikerült."); + error.status = response.status; + throw error; + } + + return data; +} + +function syncLogNextInspectionDate() { + const inspectionDate = elements.logInspectionDate.value; + const intervalDays = Number(elements.logInspectionIntervalDays.value || 30); + + if (!inspectionDate) { + return; + } + + const baseDate = new Date(`${inspectionDate}T00:00:00`); + baseDate.setDate(baseDate.getDate() + intervalDays); + elements.logNextInspectionDate.value = baseDate.toISOString().slice(0, 10); +} + +function setToday(element) { + element.value = new Date().toISOString().slice(0, 10); +} + +function calculateCountdown(nextInspectionDate) { + const normalizedDate = normalizeDateInput(nextInspectionDate); + + if (!normalizedDate) { + return { + expired: false, + warning: false, + label: "Nincs dátum", + text: "Nincs megadott határidő" + }; + } + + const targetTime = new Date(`${normalizedDate}T23:59:59`).getTime(); + const diff = targetTime - Date.now(); + const expired = diff < 0; + const absoluteDiff = Math.abs(diff); + const totalHours = Math.floor(absoluteDiff / (1000 * 60 * 60)); + const days = Math.floor(totalHours / 24); + const hours = totalHours % 24; + const warning = !expired && days <= 7; + const label = expired ? "Lejárt" : warning ? "Hamarosan esedékes" : "Rendben"; + const prefix = expired ? "Túllépés" : "Hátralévő idő"; + + return { + expired, + warning, + label, + text: `${prefix}: ${days} nap ${hours} óra` + }; +} + +function humanizeStatus(status) { + return status === "rendben" ? "Rendben" : status === "figyelmeztetes" ? "Figyelmeztetés" : "Lejárt"; +} + +function statusClassName(status) { + return status === "rendben" ? "success-chip" : status === "figyelmeztetes" ? "warning-chip" : "danger-chip"; +} + +function formatDate(value) { + const normalizedDate = normalizeDateInput(value); + return normalizedDate ? new Date(`${normalizedDate}T00:00:00`).toLocaleDateString("hu-HU") : "Nincs dátum"; +} + +function normalizeDateInput(value) { + if (!value) { + return ""; + } + + if (typeof value === "string") { + return value.includes("T") ? value.slice(0, 10) : value; + } + + const parsedDate = new Date(value); + return Number.isNaN(parsedDate.getTime()) ? "" : parsedDate.toISOString().slice(0, 10); +} + +function setFeedback(element, message, type) { + element.textContent = message; + element.className = `feedback-text ${type ? `feedback-${type}` : ""}`; +} + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} diff --git a/public/auth.js b/public/auth.js new file mode 100644 index 0000000..1fe31e6 --- /dev/null +++ b/public/auth.js @@ -0,0 +1,42 @@ +const form = document.getElementById("authForm"); +const feedback = document.getElementById("authMessage"); +const isRegisterPage = window.location.pathname === "/regisztracio"; + +form?.addEventListener("submit", handleSubmit); + +async function handleSubmit(event) { + event.preventDefault(); + + const formData = new FormData(form); + const payload = Object.fromEntries(formData.entries()); + const endpoint = isRegisterPage ? "/api/register" : "/api/login"; + + try { + const response = await fetch(endpoint, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || "A kérés nem sikerült."); + } + + setFeedback(data.message, "success"); + window.setTimeout(() => { + window.location.href = data.redirectPath; + }, 500); + } catch (error) { + setFeedback(error.message, "error"); + } +} + +function setFeedback(message, type) { + feedback.textContent = message; + feedback.className = `feedback-text ${type ? `feedback-${type}` : ""}`; +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d97a03f --- /dev/null +++ b/public/index.html @@ -0,0 +1,116 @@ + + + + + + Csödényi Felvonó Felülvizsgáló | Biztonság és szakértelem + + + +
+ + +
+
+
+

Felvonó-felülvizsgálat kompromisszumok nélkül

+

Biztonság és szakértelem a legfelsőbb szinteken

+

+ A lift nem pusztán kényelmi eszköz, hanem a mindennapi biztonság és a zavartalan közlekedés alapja. + A Csödényi Felvonó Felülvizsgálónál azért dolgozunk, hogy az Ön épületében üzemelő felvonók + megbízhatóan, szabályosan és kiszámíthatóan működjenek. +

+

+ Több éves szakmai tapasztalattal, naprakész jogszabályi ismeretekkel és korszerű diagnosztikai + eszközökkel végezzük személy- és teherfelvonók időszakos vizsgálatát, biztonságtechnikai ellenőrzését + és üzembe helyezés előtti felülvizsgálatát. +

+
+ +
+ +
+
+
+

Miért minket válasszon?

+

Megbízható partner társasházak, irodaházak és ipari létesítmények számára.

+
+
+ +
+
+

Maximális precizitás

+

A biztonság területén nem ismerünk kompromisszumot: minden ellenőrzést részletesen, dokumentáltan végzünk el.

+
+
+

Gyors és rugalmas ügyintézés

+

Az épület működéséhez igazodunk, hogy a vizsgálatok a lehető legkevesebb fennakadással járjanak.

+
+
+

Teljes körű dokumentáció

+

Átlátható, hatósági előírásoknak megfelelő jegyzőkönyveket és jól követhető szakmai összefoglalókat adunk át.

+
+
+
+ +
+
+
+

Miben állunk rendelkezésére?

+

Röviden összefoglaltuk, hogyan tudjuk támogatni az üzemeltetőket és közös képviselőket.

+
+
+ +
+
+

Törvényi előírás szerinti időszakos ellenőrzések

+

Negyedéves, féléves és éves biztonságtechnikai felülvizsgálatok a jogszabályi megfelelés érdekében.

+
+
+

Üzembe helyezés előtti vizsgálat

+

Új vagy modernizált liftek hatósági átadásának szakértői előkészítése és támogatása.

+
+
+

Kockázatértékelés és állapotfelmérés

+

Részletes műszaki helyzetkép, javítási javaslatokkal és korszerűsítési irányokkal.

+
+
+

Szakértői tanácsadás

+

Segítség közös képviselőknek, üzemeltetőknek és cégvezetőknek a vonatkozó szabványok és követelmények értelmezésében.

+
+
+
+ +
+
+

Közeleg a kötelező felülvizsgálat?

+

+ A biztonságos liftüzemeltetés és a szabályozások betartása komoly felelősség. Ne hagyja az utolsó + pillanatra a vizsgálat megszervezését. +

+
+

+ A Csödényi Felvonó Felülvizsgáló gyorsan, pontosan és a hatályos előírások szigorú követésével dolgozik, + hogy a működés zavartalan, a dokumentáció pedig kifogástalan legyen. +

+
+ Telefon: +36 30 123 4567 + E-mail: info@csodenyifelulvizsgalo.hu + Webalkalmazás: technikusok részére +
+
+
+
+ + diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..96222ac --- /dev/null +++ b/public/login.html @@ -0,0 +1,51 @@ + + + + + + Bejelentkezés | LiftEllenőr + + + +
+ + +
+
+
+

Bejelentkezés

+

A rendszer használatához jelentkezz be a felhasználói adataiddal.

+
+ +
+ + + + + + + +
+ + + +
+
+
+ + + + diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..c0f29e5 Binary files /dev/null and b/public/logo.png differ diff --git a/public/logs.html b/public/logs.html new file mode 100644 index 0000000..4f24755 --- /dev/null +++ b/public/logs.html @@ -0,0 +1,60 @@ + + + + + + Napló | LiftEllenőr + + + +
+
+
+
+ +
+

Napló

+

+
+
+
+ Vissza az alkalmazáshoz + +
+
+ +
+
+
+

Bejegyzések

+

Kereshető és rendezhető ellenőrzési napló a teljes útvonalhoz.

+
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/public/logs.js b/public/logs.js new file mode 100644 index 0000000..ff612f7 --- /dev/null +++ b/public/logs.js @@ -0,0 +1,261 @@ +const state = { + user: null, + logs: [], + logsLoaded: false, + initialLogRetryCount: 0, + searchActivated: false +}; + +const elements = { + welcomeText: document.getElementById("welcomeText"), + routeText: document.getElementById("routeText"), + logoutButton: document.getElementById("logoutButton"), + entriesList: document.getElementById("entriesList"), + searchInput: document.getElementById("searchInput"), + sortSelect: document.getElementById("sortSelect") +}; + +bindEvents(); +bootPage(); + +function bindEvents() { + elements.logoutButton.addEventListener("click", logout); + elements.searchInput.addEventListener("input", handleSearchInput); + elements.sortSelect.addEventListener("change", loadLogs); + window.addEventListener("pageshow", handlePageShow); +} + +async function bootPage() { + state.logsLoaded = false; + state.initialLogRetryCount = 0; + state.searchActivated = false; + elements.searchInput.value = ""; + renderLogs(); + + try { + const response = await apiFetch("/api/me"); + state.user = response.user; + fillUserHeader(); + await loadLogs(true); + } catch (error) { + if (error.status === 401) { + window.location.href = "/bejelentkezes"; + return; + } + + elements.welcomeText.textContent = "Betöltési hiba"; + elements.routeText.textContent = "A napló nem tölthető be. Frissítsd az oldalt vagy jelentkezz be újra."; + renderError(error.message); + } +} + +function fillUserHeader() { + const name = state.user?.fullName || "Felhasználó"; + const route = state.user?.routeName || "Nincs megadva"; + elements.welcomeText.textContent = "Napló"; + elements.routeText.textContent = `${name} · Technikusi útvonal: ${route}`; +} + +async function loadLogs(isInitialLoad = false) { + try { + const sortBy = elements.sortSelect.value || "inspectionDateDesc"; + const response = await apiFetch(`/api/logs?sortBy=${encodeURIComponent(sortBy)}`); + + state.logs = Array.isArray(response.logs) + ? response.logs.map((log) => ({ + ...log, + files: Array.isArray(log.files) ? log.files : [] + })) + : []; + + state.logsLoaded = true; + renderLogs(); + + if (isInitialLoad && !state.logs.length) { + scheduleInitialLogRetry(); + } + } catch (error) { + state.logs = []; + state.logsLoaded = true; + renderError(error.message); + + if (isInitialLoad) { + scheduleInitialLogRetry(); + } + } +} + +function renderLogs() { + if (!state.logsLoaded) { + elements.entriesList.innerHTML = ` +
+

Betöltés

+

A bejegyzések betöltése folyamatban van.

+
+ `; + return; + } + + const query = state.searchActivated ? elements.searchInput.value.trim().toLowerCase() : ""; + const visibleLogs = state.logs.filter((log) => searchableLog(log).includes(query)); + + if (!visibleLogs.length) { + elements.entriesList.innerHTML = ` +
+

Nincs találat

+

Módosítsd a keresést, vagy hozz létre új ellenőrzési bejegyzést az alkalmazás oldalon.

+
+ `; + return; + } + + elements.entriesList.innerHTML = visibleLogs + .map((log) => { + const fileMarkup = log.files.length + ? log.files + .map( + (file) => ` + + ${escapeHtml(file.originalName)} + + ` + ) + .join("") + : `Nincs PDF csatolmány`; + + return ` +
+
+ ${humanizeStatus(log.status)} + ${formatDate(log.inspectionDate)} +
+

${escapeHtml(log.elevatorName)}

+

Gyári szám: ${escapeHtml(log.serialNumber)}

+

Helyszín: ${escapeHtml(log.location)}

+

Útvonal: ${escapeHtml(log.routeName)}

+

Technikus: ${escapeHtml(log.technicianName)}

+

Következő ellenőrzés: ${formatDate(log.nextInspectionDate)}

+

Megjegyzés: ${escapeHtml(log.notes || "Nincs megjegyzés.")}

+
${fileMarkup}
+
+ `; + }) + .join(""); +} + +function renderError(message) { + elements.entriesList.innerHTML = ` +
+

Hiba

+

${escapeHtml(message)}

+
+ `; +} + +function scheduleInitialLogRetry() { + if (state.initialLogRetryCount >= 3 || state.logs.length) { + return; + } + + state.initialLogRetryCount += 1; + + window.setTimeout(() => { + state.logsLoaded = false; + renderLogs(); + loadLogs(false); + }, 500 * state.initialLogRetryCount); +} + +function handleSearchInput() { + state.searchActivated = true; + renderLogs(); +} + +function handlePageShow() { + if (state.user) { + state.logsLoaded = false; + renderLogs(); + loadLogs(); + } +} + +async function logout() { + try { + const response = await apiFetch("/api/logout", { method: "POST" }); + window.location.href = response.redirectPath; + } catch (_error) { + window.location.href = "/bejelentkezes"; + } +} + +async function apiFetch(url, options = {}) { + const headers = options.isFormData ? {} : { "Content-Type": "application/json" }; + const response = await fetch(url, { + method: options.method || "GET", + credentials: "include", + headers, + body: options.body + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : { message: "A szerver nem JSON választ adott vissza." }; + + if (!response.ok) { + const error = new Error(data.message || "A kérés nem sikerült."); + error.status = response.status; + throw error; + } + + return data; +} + +function searchableLog(log) { + return [ + log.elevatorName, + log.serialNumber, + log.location, + log.routeName, + log.technicianName, + log.status, + log.notes + ] + .join(" ") + .toLowerCase(); +} + +function humanizeStatus(status) { + return status === "rendben" ? "Rendben" : status === "figyelmeztetes" ? "Figyelmeztetés" : "Lejárt"; +} + +function statusClassName(status) { + return status === "rendben" ? "success-chip" : status === "figyelmeztetes" ? "warning-chip" : "danger-chip"; +} + +function formatDate(value) { + const normalizedDate = normalizeDateInput(value); + return normalizedDate ? new Date(`${normalizedDate}T00:00:00`).toLocaleDateString("hu-HU") : "Nincs dátum"; +} + +function normalizeDateInput(value) { + if (!value) { + return ""; + } + + if (typeof value === "string") { + return value.includes("T") ? value.slice(0, 10) : value; + } + + const parsedDate = new Date(value); + return Number.isNaN(parsedDate.getTime()) ? "" : parsedDate.toISOString().slice(0, 10); +} + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} diff --git a/public/register.html b/public/register.html new file mode 100644 index 0000000..ba09919 --- /dev/null +++ b/public/register.html @@ -0,0 +1,57 @@ + + + + + + Regisztráció | LiftEllenőr + + + +
+ + +
+
+
+

Regisztráció

+

Új technikusi hozzáférés létrehozása.

+
+ +
+ + + + + + + + + + + + + +
+ + + +
+
+
+ + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..08b1a39 --- /dev/null +++ b/public/style.css @@ -0,0 +1,788 @@ +:root { + --bg: #f3f4f6; + --panel: #ffffff; + --line: #d9dee7; + --line-strong: #c8d0dc; + --text: #1f2937; + --muted: #6b7280; + --accent: #2563eb; + --accent-dark: #1d4ed8; + --warning: #b45309; + --danger: #b91c1c; + --success: #15803d; + --shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + --radius: 12px; +} + +* { + box-sizing: border-box; +} + +html { + color-scheme: light; +} + +body { + margin: 0; + font-family: "Segoe UI", Arial, sans-serif; + font-size: 15px; + line-height: 1.5; + color: var(--text); + background: + radial-gradient(circle at top, rgba(37, 99, 235, 0.08), transparent 28%), + linear-gradient(180deg, #f7faff, var(--bg) 18%); +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + cursor: pointer; + border: 0; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.75rem 0.85rem; + background: #fff; + color: var(--text); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); +} + +textarea { + resize: vertical; +} + +label { + font-size: 0.95rem; + font-weight: 600; + color: var(--text); +} + +#pageShell { + width: min(1180px, calc(100% - 2rem)); + margin: 0 auto; + padding: 1.5rem 0 2.5rem; +} + +#heroText, +.panel-head p, +#routeText, +.muted-text, +.feedback-text, +.entry-card p, +.dashboard-card p, +.auth-link-row { + color: var(--muted); +} + +#mainLayout, +#appShell { + display: grid; + gap: 1rem; +} + +.landing-layout { + gap: 1.25rem; +} + +.auth-panel { + max-width: 560px; + margin: 0 auto; +} + +.panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 1.25rem; + box-shadow: var(--shadow); +} + +.landing-header { + position: sticky; + top: 1rem; + z-index: 2; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98)), + var(--panel); + backdrop-filter: blur(8px); +} + +#siteHeader { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + border-color: #d6e2f7; + background: + linear-gradient(135deg, rgba(239, 245, 255, 0.96), rgba(255, 255, 255, 0.98)), + var(--panel); +} + +.brand-block { + display: inline-flex; + align-items: center; + gap: 0.9rem; + min-width: 0; +} + +.brand-link { + color: inherit; + text-decoration: none; +} + +.brand-copy { + min-width: 0; +} + +.brand-logo { + width: 52px; + height: 52px; + object-fit: contain; + border-radius: 14px; + flex: 0 0 auto; + background: #fff; + box-shadow: 0 10px 22px rgba(37, 99, 235, 0.14); +} + +.brand-name { + margin: 0; + font-size: 1.1rem; + font-weight: 700; + color: var(--accent-dark); +} + +.brand-subtitle { + margin: 0.15rem 0 0; + color: var(--muted); + font-size: 0.92rem; +} + +.landing-header .brand-block { + gap: 1rem; +} + +.landing-header .brand-copy { + display: grid; + gap: 0.2rem; +} + +.landing-header .brand-logo { + width: 68px; + height: 68px; + padding: 0.2rem; + border-radius: 18px; +} + +.landing-header .brand-name { + line-height: 1.15; +} + +.top-nav { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.nav-link { + display: inline-flex; + align-items: center; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.55rem 0.8rem; + color: var(--text); + background: #fff; + text-decoration: none; + font-weight: 600; +} + +.nav-link:hover { + background: #f8fafc; +} + +.nav-link-active { + border-color: var(--line-strong); + background: #eef4ff; + color: var(--accent-dark); +} + +.section-heading { + display: grid; + gap: 0.2rem; +} + +.page-title { + margin: 0; + font-size: 1.5rem; + font-weight: 700; +} + +.landing-kicker { + margin: 0 0 0.6rem; + color: var(--accent-dark); + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.landing-title { + margin: 0; + font-size: clamp(2rem, 4vw, 3.4rem); + line-height: 1.05; + max-width: 12ch; +} + +.landing-lead { + margin: 0; + font-size: 1.02rem; + color: var(--muted); + max-width: 68ch; +} + +.page-text { + margin: 0; + color: var(--muted); +} + +.panel-head { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1rem; +} + +.panel-actions, +.inline-actions, +.device-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.panel-head h2, +.entry-card h3, +.dashboard-card h3 { + margin: 0; +} + +.panel-head h2 { + font-size: 1.15rem; +} + +.tab-button, +.ghost-button, +.primary-button, +.secondary-button { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + padding: 0.7rem 1rem; + font-weight: 600; + text-decoration: none; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +.tab-button, +.ghost-button { + border: 1px solid var(--line); + background: #fff; + color: var(--text); +} + +.primary-button { + background: var(--accent); + color: #fff; +} + +.primary-button:hover { + background: var(--accent-dark); +} + +.secondary-button { + background: #eef4ff; + color: var(--accent-dark); + border: 1px solid #cfe0ff; +} + +.secondary-button:hover { + background: #e3edff; +} + +.ghost-button:hover, +.tab-button:hover { + background: #f8fafc; +} + +.app-topbar .ghost-button { + border-color: #bfd3fb; + background: rgba(255, 255, 255, 0.88); +} + +.app-topbar .ghost-button:hover { + background: #eff5ff; +} + +.compact-button { + padding: 0.55rem 0.8rem; + font-size: 0.92rem; +} + +.auth-form, +#logForm { + display: grid; + gap: 0.85rem; +} + +#deviceForm { + display: grid; + gap: 0.85rem; +} + +.hidden { + display: none !important; +} + +#topBar { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.app-topbar { + border-color: #cfe0ff; + background: + linear-gradient(135deg, rgba(37, 99, 235, 0.12), rgba(255, 255, 255, 0.96) 28%), + var(--panel); +} + +.topbar-heading { + display: flex; + align-items: center; + gap: 0.9rem; + min-width: 0; +} + +.topbar-logo { + width: 50px; + height: 50px; + object-fit: contain; + border-radius: 14px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 8px 18px rgba(37, 99, 235, 0.12); + padding: 0.2rem; + flex: 0 0 auto; +} + +.landing-hero { + display: grid; + grid-template-columns: minmax(0, 1.8fr) minmax(240px, 0.8fr); + gap: 1rem; + align-items: stretch; + padding: 1.5rem; + background: + radial-gradient(circle at top right, rgba(37, 99, 235, 0.1), transparent 32%), + linear-gradient(180deg, #ffffff, #f7f9fc); +} + +.landing-hero-copy { + display: grid; + gap: 1rem; +} + +.landing-hero-logo { + width: min(100%, 320px); + max-height: 320px; + justify-self: center; + align-self: center; + object-fit: contain; + border-radius: 24px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 18px 36px rgba(37, 99, 235, 0.16); + padding: 0.6rem; +} + +.hero-sidecard { + display: grid; + align-content: start; + gap: 0.9rem; + padding: 1rem; + border: 1px solid #dbe4f0; + border-radius: 14px; + background: linear-gradient(180deg, #eef4ff, #ffffff); +} + +.hero-sidecard-label { + margin: 0; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent-dark); +} + +.hero-sidecard-text { + margin: 0; + color: var(--text); + font-weight: 600; +} + +.landing-login-link { + padding: 0.45rem 0.75rem; + font-size: 0.92rem; + border-color: #bfd3fb; + color: var(--accent-dark); + background: rgba(255, 255, 255, 0.9); +} + +.landing-feature-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.landing-feature-card, +.landing-service-item { + border: 1px solid var(--line); + border-radius: 12px; + background: linear-gradient(180deg, #ffffff, #f8fafc); +} + +.landing-feature-card { + padding: 1rem; +} + +.landing-feature-card h3, +.landing-service-item h3 { + margin: 0 0 0.55rem; +} + +.landing-feature-card p, +.landing-service-item p, +.landing-cta-text { + margin: 0; + color: var(--muted); +} + +.landing-service-list { + display: grid; + gap: 0.9rem; +} + +.landing-service-item { + padding: 1rem 1rem 1rem 1.15rem; + border-left: 4px solid #cfe0ff; +} + +.landing-cta { + display: grid; + gap: 1rem; + background: + linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(255, 255, 255, 1) 38%), + #fff; +} + +.landing-contact-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem 1.25rem; + color: var(--text); + font-weight: 600; +} + +.dashboard-grid, +.entries-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.dashboard-card, +.entry-card { + padding: 1rem; + border-radius: 10px; + background: #fff; + border: 1px solid var(--line); + min-height: 100%; +} + +.empty-card { + display: grid; + place-items: center; + text-align: center; + min-height: 180px; +} + +.card-topline { + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: center; + margin-bottom: 0.8rem; + flex-wrap: wrap; +} + +.status-chip, +.serial-chip { + display: inline-flex; + align-items: center; + border-radius: 999px; + padding: 0.3rem 0.65rem; + font-size: 0.82rem; + font-weight: 600; +} + +.serial-chip { + background: #f3f4f6; + color: var(--text); +} + +.success-chip { + background: #eaf7ef; + color: var(--success); +} + +.warning-chip { + background: #fff4e5; + color: var(--warning); +} + +.danger-chip { + background: #fdecec; + color: var(--danger); +} + +.countdown-box { + margin: 1rem 0 0.75rem; + padding: 0.85rem 0.95rem; + border-radius: 8px; + background: #f8fafc; + border: 1px solid var(--line); + font-size: 1rem; + font-weight: 700; +} + +.form-grid, +#entryControls { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +#entryControls { + margin-bottom: 0.75rem; +} + +.field-wrap { + display: grid; + gap: 0.45rem; +} + +.readonly-input { + background: #f8fafc; + color: var(--muted); +} + +.field-wide { + grid-column: 1 / -1; +} + +.form-actions { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + flex-wrap: wrap; + margin-top: 0.5rem; +} + +.feedback-text { + min-height: 1.25rem; + margin: 0; +} + +.feedback-success { + color: var(--success); +} + +.feedback-error { + color: var(--danger); +} + +.file-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.9rem; +} + +.summary-card { + padding: 0.95rem 1rem; + border-radius: 10px; + border: 1px solid var(--line); + background: #f8fafc; + color: var(--text); + line-height: 1.6; +} + +.device-card { + display: grid; +} + +.device-actions { + margin-top: 1rem; +} + +.modal-open { + overflow: hidden; +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 20; + display: grid; + place-items: center; + padding: 1rem; + background: rgba(15, 23, 42, 0.52); +} + +.modal-panel { + width: min(760px, 100%); + max-height: calc(100vh - 2rem); + overflow: auto; + border: 1px solid #d6e2f7; + border-radius: 16px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98)), + #fff; + box-shadow: 0 24px 48px rgba(15, 23, 42, 0.24); + padding: 1.25rem; +} + +.modal-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.modal-close { + flex: 0 0 auto; +} + +.file-link { + display: inline-flex; + align-items: center; + padding: 0.45rem 0.7rem; + border-radius: 8px; + border: 1px solid var(--line); + background: #f8fafc; + color: var(--accent-dark); + text-decoration: none; +} + +.file-link:hover { + background: #eef4ff; + border-color: #cfe0ff; +} + +.auth-link-row { + margin: 0.4rem 0 0; +} + +.auth-link-row a { + color: var(--accent-dark); + font-weight: 600; + text-decoration: none; +} + +.auth-link-row a:hover { + text-decoration: underline; +} + +@media (max-width: 960px) { + .landing-hero, + .landing-feature-grid, + .dashboard-grid, + .entries-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + #pageShell { + width: min(100% - 1rem, 1180px); + padding-top: 1rem; + } + + #siteHeader, + #topBar, + .panel-head, + .form-actions, + .modal-head { + display: grid; + align-items: start; + } + + .brand-block, + .topbar-heading { + align-items: start; + } + + .landing-hero, + .landing-feature-grid, + .form-grid, + #entryControls, + .dashboard-grid, + .entries-grid { + grid-template-columns: 1fr; + } + + #mainTitle { + max-width: none; + font-size: 1.6rem; + } + + .landing-title { + max-width: none; + } + + .landing-hero-logo { + width: min(100%, 220px); + max-height: 220px; + } + + .brand-logo, + .topbar-logo { + width: 44px; + height: 44px; + } +} diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100644 index 0000000..bb3dbb1 --- /dev/null +++ b/scripts/seed.js @@ -0,0 +1,419 @@ +const bcrypt = require("bcrypt"); +const { Pool } = require("pg"); + +const DATABASE_URL = process.env.DATABASE_URL || "postgresql://lift_admin:lift_secret@127.0.0.1:5432/lift_manager"; +const SALT_ROUNDS = 10; +const DEFAULT_PASSWORD = process.env.SEED_PASSWORD || "teszt123"; +const RUN_TOKEN = Date.now().toString(36).slice(-6); + +const ROUTES = [ + "Észak-Buda", + "Dél-Buda", + "Észak-Pest", + "Dél-Pest", + "Belváros", + "Külváros", + "Keleti zóna", + "Nyugati zóna" +]; + +const FIRST_NAMES = [ + "Ádám", + "Bence", + "Csaba", + "Dávid", + "Erika", + "Ferenc", + "Gábor", + "Hanna", + "István", + "Judit", + "Katalin", + "Levente", + "Márk", + "Noémi", + "Orsolya", + "Péter", + "Réka", + "Sándor", + "Tamás", + "Zoltán" +]; + +const LAST_NAMES = [ + "Kovács", + "Szabó", + "Tóth", + "Nagy", + "Varga", + "Kiss", + "Molnár", + "Németh", + "Farkas", + "Balogh", + "Takács", + "Juhász" +]; + +const STREETS = [ + "Kossuth Lajos utca", + "Rákóczi út", + "Bartók Béla út", + "Alkotás utca", + "Petőfi Sándor utca", + "Szent István körút", + "Andrássy út", + "József Attila utca", + "Kerepesi út", + "Váci út", + "Móricz Zsigmond körtér", + "Fehérvári út" +]; + +const BUILDING_TYPES = [ + "Társasház", + "Irodaház", + "Rendelőintézet", + "Szálloda", + "Kereskedelmi központ", + "Logisztikai csarnok", + "Önkormányzati épület", + "Parkolóház" +]; + +const STATUS_VALUES = ["rendben", "rendben", "rendben", "figyelmeztetes", "lejart"]; +const NOTE_TEMPLATES = [ + "Rendszeres időszakos ellenőrzés, jelentős eltérés nem tapasztalható.", + "Ajtózár finombeállítás javasolt a következő szerviz alkalmával.", + "A gépház tiszta, az üzemi paraméterek megfelelő tartományban vannak.", + "Kisebb kopás látható a vezetősínek környezetében, megfigyelés javasolt.", + "A vészvilágítás működik, a dokumentáció rendben.", + "Szintbeállítás megfelelő, utasoldali panasz nem érkezett.", + "Biztonsági áramkör ellenőrizve, működés megfelelő.", + "Esztétikai sérülések láthatók a fülkében, műszaki hibát nem okoznak." +]; + +const pool = new Pool({ + connectionString: DATABASE_URL +}); + +const options = parseArgs(process.argv.slice(2)); + +main().catch((error) => { + console.error("A seed futtatása sikertelen:", error.message); + process.exitCode = 1; +}).finally(async () => { + await pool.end(); +}); + +async function main() { + await initializeDatabase(); + + if (options.reset) { + await resetDatabase(); + } + + const passwordHash = await bcrypt.hash(DEFAULT_PASSWORD, SALT_ROUNDS); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + const createdUsers = []; + const createdElevators = []; + let createdLogs = 0; + + for (let routeIndex = 0; routeIndex < options.routeCount; routeIndex += 1) { + const routeName = ROUTES[routeIndex % ROUTES.length]; + + for (let userIndex = 0; userIndex < options.usersPerRoute; userIndex += 1) { + const user = await createUser(client, routeName, routeIndex, userIndex, passwordHash); + createdUsers.push(user); + } + + const routeUsers = createdUsers.filter((user) => user.route_name === routeName); + + for (let elevatorIndex = 0; elevatorIndex < options.elevatorsPerRoute; elevatorIndex += 1) { + const elevator = await createElevator(client, routeName, routeIndex, elevatorIndex); + createdElevators.push(elevator); + createdLogs += await createLogsForElevator(client, elevator, routeUsers, elevatorIndex, options.logsPerElevator); + } + } + + await client.query("COMMIT"); + + console.log("Seed kész."); + console.log(`Felhasználók: ${createdUsers.length}`); + console.log(`Eszközök: ${createdElevators.length}`); + console.log(`Naplóbejegyzések: ${createdLogs}`); + console.log(`Teszt jelszó: ${DEFAULT_PASSWORD}`); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } +} + +function parseArgs(args) { + const parsed = { + reset: args.includes("--reset"), + routeCount: 6, + usersPerRoute: 3, + elevatorsPerRoute: 18, + logsPerElevator: 14 + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const nextValue = args[index + 1]; + + if (arg === "--routes") { + parsed.routeCount = toPositiveInt(nextValue, parsed.routeCount); + } + + if (arg === "--users-per-route") { + parsed.usersPerRoute = toPositiveInt(nextValue, parsed.usersPerRoute); + } + + if (arg === "--elevators-per-route") { + parsed.elevatorsPerRoute = toPositiveInt(nextValue, parsed.elevatorsPerRoute); + } + + if (arg === "--logs-per-elevator") { + parsed.logsPerElevator = toPositiveInt(nextValue, parsed.logsPerElevator); + } + } + + return parsed; +} + +function toPositiveInt(value, fallback) { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; +} + +async function initializeDatabase() { + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + full_name TEXT NOT NULL, + route_name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS elevators ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + serial_number TEXT NOT NULL UNIQUE, + location TEXT NOT NULL, + route_name TEXT NOT NULL, + last_inspection_date DATE, + next_inspection_date DATE, + inspection_interval_days INTEGER NOT NULL DEFAULT 30, + last_status TEXT NOT NULL DEFAULT 'rendben', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS logs ( + id SERIAL PRIMARY KEY, + elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL, + elevator_name TEXT NOT NULL, + serial_number TEXT NOT NULL, + location TEXT NOT NULL, + route_name TEXT NOT NULL, + inspection_date DATE NOT NULL, + next_inspection_date DATE NOT NULL, + inspection_interval_days INTEGER NOT NULL DEFAULT 30, + status TEXT NOT NULL, + notes TEXT NOT NULL DEFAULT '', + technician_name TEXT NOT NULL, + created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS log_files ( + id SERIAL PRIMARY KEY, + log_id INTEGER NOT NULL REFERENCES logs(id) ON DELETE CASCADE, + original_name TEXT NOT NULL, + stored_name TEXT NOT NULL, + size INTEGER NOT NULL, + mime_type TEXT NOT NULL + ); + `); + + await pool.query(` + ALTER TABLE logs + ADD COLUMN IF NOT EXISTS elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL; + `); +} + +async function resetDatabase() { + await pool.query(` + TRUNCATE TABLE log_files, logs, elevators, users + RESTART IDENTITY CASCADE; + `); +} + +async function createUser(client, routeName, routeIndex, userIndex, passwordHash) { + const firstName = FIRST_NAMES[(routeIndex * 3 + userIndex) % FIRST_NAMES.length]; + const lastName = LAST_NAMES[(routeIndex * 5 + userIndex) % LAST_NAMES.length]; + const fullName = `${lastName} ${firstName}`; + const username = normalizeUsername(`${lastName}.${firstName}.${routeIndex + 1}.${userIndex + 1}.${RUN_TOKEN}`); + + const result = await client.query( + `INSERT INTO users (username, password_hash, full_name, route_name) + VALUES ($1, $2, $3, $4) + RETURNING id, username, full_name, route_name`, + [username, passwordHash, fullName, routeName] + ); + + return result.rows[0]; +} + +async function createElevator(client, routeName, routeIndex, elevatorIndex) { + const buildingType = BUILDING_TYPES[(routeIndex + elevatorIndex) % BUILDING_TYPES.length]; + const street = STREETS[(routeIndex * 7 + elevatorIndex) % STREETS.length]; + const buildingNumber = 10 + ((routeIndex * 13 + elevatorIndex * 3) % 89); + const elevatorName = `${buildingType} ${routeIndex + 1}/${elevatorIndex + 1}. lift`; + const serialNumber = buildSerialNumber(routeIndex, elevatorIndex); + const location = `Budapest, ${street} ${buildingNumber}.`; + + const result = await client.query( + `INSERT INTO elevators ( + name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at`, + [elevatorName, serialNumber, location, routeName, null, null, 30, "rendben"] + ); + + return result.rows[0]; +} + +async function createLogsForElevator(client, elevator, routeUsers, elevatorIndex, logCount) { + const createdLogs = []; + const latestLogProfile = buildLatestLogProfile(elevatorIndex); + + for (let logIndex = 0; logIndex < logCount; logIndex += 1) { + const isLatestLog = logIndex === logCount - 1; + const logProfile = isLatestLog ? latestLogProfile : buildHistoricalLogProfile(elevatorIndex, logIndex); + const user = routeUsers[(elevatorIndex + logIndex) % routeUsers.length]; + const inspectionDate = dateToIso(logProfile.inspectionDate); + const nextInspectionDate = dateToIso(logProfile.nextInspectionDate); + + const result = await client.query( + `INSERT INTO logs ( + elevator_id, elevator_name, serial_number, location, route_name, inspection_date, + next_inspection_date, inspection_interval_days, status, notes, + technician_name, created_by_user_id, created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING id`, + [ + elevator.id, + elevator.name, + elevator.serial_number, + elevator.location, + elevator.route_name, + inspectionDate, + nextInspectionDate, + logProfile.inspectionIntervalDays, + logProfile.status, + logProfile.notes, + user.full_name, + user.id, + logProfile.createdAt.toISOString() + ] + ); + + createdLogs.push(result.rows[0].id); + } + + await client.query( + `UPDATE elevators + SET last_inspection_date = $1, + next_inspection_date = $2, + inspection_interval_days = $3, + last_status = $4, + updated_at = $5 + WHERE id = $6`, + [ + dateToIso(latestLogProfile.inspectionDate), + dateToIso(latestLogProfile.nextInspectionDate), + latestLogProfile.inspectionIntervalDays, + latestLogProfile.status, + latestLogProfile.createdAt.toISOString(), + elevator.id + ] + ); + + return createdLogs.length; +} + +function buildLatestLogProfile(elevatorIndex) { + const today = startOfToday(); + const nextInspectionOffset = elevatorIndex % 10; + const intervalDays = 30 + ((elevatorIndex % 4) * 15); + const inspectionDate = addDays(today, -((elevatorIndex % 18) + 3)); + const nextInspectionDate = addDays(today, nextInspectionOffset); + const status = nextInspectionOffset <= 1 ? "figyelmeztetes" : "rendben"; + + return { + inspectionDate, + nextInspectionDate, + inspectionIntervalDays: intervalDays, + status, + notes: NOTE_TEMPLATES[elevatorIndex % NOTE_TEMPLATES.length], + createdAt: new Date(`${dateToIso(inspectionDate)}T09:00:00Z`) + }; +} + +function buildHistoricalLogProfile(elevatorIndex, logIndex) { + const intervalDays = 30 + (((elevatorIndex + logIndex) % 4) * 15); + const inspectionDate = addDays(startOfToday(), -((logIndex + 2) * intervalDays)); + const nextInspectionDate = addDays(inspectionDate, intervalDays); + const status = STATUS_VALUES[(elevatorIndex + logIndex) % STATUS_VALUES.length]; + + return { + inspectionDate, + nextInspectionDate, + inspectionIntervalDays: intervalDays, + status, + notes: NOTE_TEMPLATES[(elevatorIndex + logIndex) % NOTE_TEMPLATES.length], + createdAt: new Date(`${dateToIso(inspectionDate)}T08:30:00Z`) + }; +} + +function buildSerialNumber(routeIndex, elevatorIndex) { + const routePart = String(routeIndex + 1).padStart(2, "0"); + const elevatorPart = String(elevatorIndex + 1).padStart(3, "0"); + return `LIFT-${RUN_TOKEN}-${routePart}-${elevatorPart}`; +} + +function normalizeUsername(value) { + return value + .normalize("NFD") + .replaceAll(/[\u0300-\u036f]/g, "") + .replaceAll(/[^a-zA-Z0-9.]/g, "") + .toLowerCase(); +} + +function startOfToday() { + const now = new Date(); + return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); +} + +function addDays(date, days) { + const copy = new Date(date.getTime()); + copy.setUTCDate(copy.getUTCDate() + days); + return copy; +} + +function dateToIso(date) { + return date.toISOString().slice(0, 10); +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..c521d36 --- /dev/null +++ b/server.js @@ -0,0 +1,944 @@ +const express = require("express"); +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const multer = require("multer"); +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); +const { Pool } = require("pg"); + +const app = express(); +const PORT = Number(process.env.PORT || 3000); +const JWT_SECRET = process.env.JWT_SECRET || "elevator-manager-secret"; +const DATABASE_URL = process.env.DATABASE_URL || "postgresql://lift_admin:lift_secret@127.0.0.1:5432/lift_manager"; +const SALT_ROUNDS = 10; +const MAX_FILES = 8; +const MAX_FILE_SIZE = 10 * 1024 * 1024; +const COOKIE_NAME = "liftToken"; +const PUBLIC_DIR = path.join(__dirname, "public"); +const DATA_DIR = path.join(__dirname, "data"); +const UPLOAD_DIR = path.join(__dirname, "uploads"); +const LEGACY_DATA_FILE = path.join(DATA_DIR, "store.json"); + +const pool = new Pool({ + connectionString: DATABASE_URL +}); + +ensureDirectories(); + +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(PUBLIC_DIR)); + +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, UPLOAD_DIR), + filename: (_req, file, cb) => { + const extension = path.extname(file.originalname).toLowerCase(); + cb(null, `${Date.now()}-${crypto.randomUUID()}${extension}`); + } +}); + +const upload = multer({ + storage, + limits: { + fileSize: MAX_FILE_SIZE, + files: MAX_FILES + }, + fileFilter: (_req, file, cb) => { + const isPdf = file.mimetype === "application/pdf" || path.extname(file.originalname).toLowerCase() === ".pdf"; + cb(isPdf ? null : new Error("Csak PDF fájl tölthető fel."), isPdf); + } +}); + +app.get("/", (_req, res) => { + res.sendFile(path.join(PUBLIC_DIR, "index.html")); +}); + +app.get("/bejelentkezes", requireGuestPage, (_req, res) => { + res.sendFile(path.join(PUBLIC_DIR, "login.html")); +}); + +app.get("/regisztracio", requireGuestPage, (_req, res) => { + res.sendFile(path.join(PUBLIC_DIR, "register.html")); +}); + +app.get("/alkalmazas", requirePageAuth, (_req, res) => { + res.sendFile(path.join(PUBLIC_DIR, "app.html")); +}); + +app.get("/naplo", requirePageAuth, (_req, res) => { + res.sendFile(path.join(PUBLIC_DIR, "logs.html")); +}); + +app.get("/api/health", async (_req, res) => { + const result = await pool.query("SELECT NOW() AS now"); + res.json({ message: "A rendszer működik.", dbTime: result.rows[0].now }); +}); + +app.post("/api/register", async (req, res) => { + try { + const username = req.body.username?.trim().toLowerCase(); + const password = req.body.password?.trim(); + const fullName = req.body.fullName?.trim(); + const routeName = req.body.routeName?.trim(); + + if (!username || !password || !fullName || !routeName) { + return res.status(400).json({ message: "Minden regisztrációs mező kötelező." }); + } + + const existingUser = await pool.query("SELECT id FROM users WHERE username = $1", [username]); + + if (existingUser.rowCount) { + return res.status(409).json({ message: "Ez a felhasználónév már foglalt." }); + } + + const passwordHash = await bcrypt.hash(password, SALT_ROUNDS); + + await pool.query( + `INSERT INTO users (username, password_hash, full_name, route_name) + VALUES ($1, $2, $3, $4)`, + [username, passwordHash, fullName, routeName] + ); + + return res.status(201).json({ + message: "Sikeres regisztráció. Most már bejelentkezhetsz.", + redirectPath: "/bejelentkezes" + }); + } catch (_error) { + return res.status(500).json({ message: "Hiba történt a regisztráció során." }); + } +}); + +app.post("/api/login", async (req, res) => { + try { + const username = req.body.username?.trim().toLowerCase(); + const password = req.body.password?.trim(); + + if (!username || !password) { + return res.status(400).json({ message: "Felhasználónév és jelszó kötelező." }); + } + + const result = await pool.query( + `SELECT id, username, password_hash, full_name, route_name + FROM users + WHERE username = $1`, + [username] + ); + + if (!result.rowCount) { + return res.status(401).json({ message: "Hibás bejelentkezési adatok." }); + } + + const user = mapUserRow(result.rows[0]); + const passwordMatches = await bcrypt.compare(password, result.rows[0].password_hash); + + if (!passwordMatches) { + return res.status(401).json({ message: "Hibás bejelentkezési adatok." }); + } + + setAuthCookie(res, createToken(user)); + + return res.json({ + message: "Sikeres bejelentkezés.", + redirectPath: "/alkalmazas", + user + }); + } catch (_error) { + return res.status(500).json({ message: "Hiba történt a bejelentkezés során." }); + } +}); + +app.post("/api/logout", (_req, res) => { + clearAuthCookie(res); + return res.json({ message: "Sikeres kijelentkezés.", redirectPath: "/bejelentkezes" }); +}); + +app.get("/api/me", authenticateRequest, (req, res) => { + res.json({ user: req.user }); +}); + +app.get("/api/dashboard", authenticateRequest, async (req, res) => { + const result = await pool.query( + `SELECT id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at + FROM elevators + WHERE route_name = $1 + AND next_inspection_date IS NOT NULL + AND next_inspection_date >= CURRENT_DATE + AND next_inspection_date <= CURRENT_DATE + INTERVAL '7 days' + ORDER BY next_inspection_date ASC NULLS LAST, id DESC`, + [req.user.routeName] + ); + + res.json({ + routeName: req.user.routeName, + elevators: result.rows.map(mapElevatorRow) + }); +}); + +app.get("/api/elevators", authenticateRequest, async (req, res) => { + const result = await pool.query( + `SELECT id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at + FROM elevators + WHERE route_name = $1 + ORDER BY name ASC, id DESC`, + [req.user.routeName] + ); + + res.json({ + routeName: req.user.routeName, + elevators: result.rows.map(mapElevatorRow) + }); +}); + +app.post("/api/elevators", authenticateRequest, async (req, res) => { + const payload = normalizeElevatorPayload(req.body, req.user); + const validationMessage = validateElevatorPayload(payload); + + if (validationMessage) { + return res.status(400).json({ message: validationMessage }); + } + + const result = await pool.query( + `INSERT INTO elevators ( + name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at`, + [ + payload.name, + payload.serialNumber, + payload.location, + payload.routeName, + payload.lastInspectionDate, + payload.nextInspectionDate, + payload.inspectionIntervalDays, + payload.status + ] + ); + + res.status(201).json({ + message: "Az eszköz sikeresen létre lett hozva.", + elevator: mapElevatorRow(result.rows[0]) + }); +}); + +app.put("/api/elevators/:elevatorId", authenticateRequest, async (req, res) => { + const elevatorId = Number(req.params.elevatorId); + + if (!Number.isInteger(elevatorId) || elevatorId <= 0) { + return res.status(400).json({ message: "Érvénytelen eszközazonosító." }); + } + + const payload = normalizeElevatorPayload(req.body, req.user); + const validationMessage = validateElevatorPayload(payload); + + if (validationMessage) { + return res.status(400).json({ message: validationMessage }); + } + + const result = await pool.query( + `UPDATE elevators + SET name = $1, + serial_number = $2, + location = $3, + route_name = $4, + last_inspection_date = $5, + next_inspection_date = $6, + inspection_interval_days = $7, + last_status = $8, + updated_at = NOW() + WHERE id = $9 AND route_name = $10 + RETURNING id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at`, + [ + payload.name, + payload.serialNumber, + payload.location, + payload.routeName, + payload.lastInspectionDate, + payload.nextInspectionDate, + payload.inspectionIntervalDays, + payload.status, + elevatorId, + req.user.routeName + ] + ); + + if (!result.rowCount) { + return res.status(404).json({ message: "A keresett eszköz nem található." }); + } + + res.json({ + message: "Az eszköz adatai frissültek.", + elevator: mapElevatorRow(result.rows[0]) + }); +}); + +app.get("/api/logs", authenticateRequest, async (req, res) => { + const search = req.query.search?.trim().toLowerCase() || ""; + const sortBy = req.query.sortBy?.trim() || "inspectionDateDesc"; + const params = [req.user.routeName]; + let whereClause = "WHERE l.route_name = $1"; + + if (search) { + params.push(`%${search}%`); + whereClause += ` AND LOWER(CONCAT_WS(' ', l.elevator_name, l.serial_number, l.location, l.route_name, l.technician_name, l.notes, l.status)) LIKE $2`; + } + + const result = await pool.query( + `SELECT l.id, l.elevator_id, l.elevator_name, l.serial_number, l.location, l.route_name, l.inspection_date, + l.next_inspection_date, l.inspection_interval_days, l.status, l.notes, + l.technician_name, l.created_by_user_id, l.created_at, + COALESCE( + JSON_AGG( + JSON_BUILD_OBJECT( + 'id', lf.id, + 'originalName', lf.original_name, + 'size', lf.size, + 'downloadPath', '/api/logs/' || l.id || '/files/' || lf.id + ) + ORDER BY lf.id + ) FILTER (WHERE lf.id IS NOT NULL), + '[]'::json + ) AS files + FROM logs l + LEFT JOIN log_files lf ON lf.log_id = l.id + ${whereClause} + GROUP BY l.id + ORDER BY ${buildLogOrder(sortBy)}`, + params + ); + + res.json({ logs: result.rows.map(mapLogRow) }); +}); + +app.post("/api/logs", authenticateRequest, upload.array("pdfs", MAX_FILES), async (req, res) => { + const uploadedFiles = req.files || []; + const client = await pool.connect(); + + try { + const payload = normalizeLogPayload(req.body, req.user); + const validationMessage = validateLogPayload(payload); + + if (validationMessage) { + removeUploadedFiles(uploadedFiles); + return res.status(400).json({ message: validationMessage }); + } + + await client.query("BEGIN"); + + let elevatorRow = null; + + if (payload.elevatorId) { + elevatorRow = await findElevatorById(client, payload.elevatorId, req.user.routeName); + + if (!elevatorRow) { + removeUploadedFiles(uploadedFiles); + return res.status(404).json({ message: "A kiválasztott eszköz nem található." }); + } + + const updatedElevator = await client.query( + `UPDATE elevators + SET last_inspection_date = $1, + next_inspection_date = $2, + inspection_interval_days = $3, + last_status = $4, + updated_at = NOW() + WHERE id = $5 + RETURNING id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at`, + [ + payload.inspectionDate, + payload.nextInspectionDate, + payload.inspectionIntervalDays, + payload.status, + payload.elevatorId + ] + ); + + elevatorRow = updatedElevator.rows[0]; + } else { + const upsertedElevator = await client.query( + `INSERT INTO elevators ( + name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (serial_number) + DO UPDATE SET + name = EXCLUDED.name, + location = EXCLUDED.location, + route_name = EXCLUDED.route_name, + last_inspection_date = EXCLUDED.last_inspection_date, + next_inspection_date = EXCLUDED.next_inspection_date, + inspection_interval_days = EXCLUDED.inspection_interval_days, + last_status = EXCLUDED.last_status, + updated_at = NOW() + RETURNING id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at`, + [ + payload.elevatorName, + payload.serialNumber, + payload.location, + payload.routeName, + payload.inspectionDate, + payload.nextInspectionDate, + payload.inspectionIntervalDays, + payload.status + ] + ); + + elevatorRow = upsertedElevator.rows[0]; + } + + const logInsert = await client.query( + `INSERT INTO logs ( + elevator_id, elevator_name, serial_number, location, route_name, inspection_date, + next_inspection_date, inspection_interval_days, status, notes, + technician_name, created_by_user_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING id`, + [ + elevatorRow.id, + elevatorRow.name, + elevatorRow.serial_number, + elevatorRow.location, + elevatorRow.route_name, + payload.inspectionDate, + payload.nextInspectionDate, + payload.inspectionIntervalDays, + payload.status, + payload.notes, + req.user.fullName, + req.user.id + ] + ); + + const logId = logInsert.rows[0].id; + + for (const file of uploadedFiles) { + const originalName = normalizeUploadedFilename(file.originalname); + + await client.query( + `INSERT INTO log_files (log_id, original_name, stored_name, size, mime_type) + VALUES ($1, $2, $3, $4, $5)`, + [logId, originalName, file.filename, file.size, file.mimetype] + ); + } + + await client.query("COMMIT"); + + res.status(201).json({ + message: "Az ellenőrzési bejegyzés sikeresen mentve lett.", + log: await fetchLogById(logId) + }); + } catch (_error) { + await client.query("ROLLBACK"); + removeUploadedFiles(uploadedFiles); + res.status(500).json({ message: "Hiba történt a bejegyzés mentése közben." }); + } finally { + client.release(); + } +}); + +app.get("/api/logs/:logId/files/:fileId", authenticateRequest, async (req, res) => { + const logId = Number(req.params.logId); + const fileId = Number(req.params.fileId); + const result = await pool.query( + `SELECT stored_name, original_name + FROM log_files + WHERE id = $1 AND log_id = $2`, + [fileId, logId] + ); + + if (!result.rowCount) { + return res.status(404).json({ message: "A keresett fájl nem található." }); + } + + const file = result.rows[0]; + const absolutePath = path.join(UPLOAD_DIR, file.stored_name); + + if (!fs.existsSync(absolutePath)) { + return res.status(404).json({ message: "A fájl már nem érhető el." }); + } + + return res.download(absolutePath, normalizeUploadedFilename(file.original_name)); +}); + +app.use((error, req, res, _next) => { + removeUploadedFiles(req.files || []); + const isMulterError = error instanceof multer.MulterError; + const message = isMulterError + ? "A PDF feltöltés sikertelen volt. Ellenőrizd a fájlméretet és a darabszámot." + : error.message || "Váratlan szerverhiba történt."; + + res.status(400).json({ message }); +}); + +startServer(); + +async function startServer() { + try { + await initializeDatabase(); + await migrateLegacyJsonIfNeeded(); + + app.listen(PORT, () => { + console.log(`A liftellenőrzési kezelő fut a(z) http://localhost:${PORT} címen.`); + }); + } catch (error) { + console.error("Nem sikerült elindítani a szervert:", error.message); + process.exit(1); + } +} + +function ensureDirectories() { + [DATA_DIR, UPLOAD_DIR, PUBLIC_DIR].forEach((directory) => { + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + }); +} + +async function initializeDatabase() { + await pool.query(` + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + full_name TEXT NOT NULL, + route_name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS elevators ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + serial_number TEXT NOT NULL UNIQUE, + location TEXT NOT NULL, + route_name TEXT NOT NULL, + last_inspection_date DATE, + next_inspection_date DATE, + inspection_interval_days INTEGER NOT NULL DEFAULT 30, + last_status TEXT NOT NULL DEFAULT 'rendben', + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS logs ( + id SERIAL PRIMARY KEY, + elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL, + elevator_name TEXT NOT NULL, + serial_number TEXT NOT NULL, + location TEXT NOT NULL, + route_name TEXT NOT NULL, + inspection_date DATE NOT NULL, + next_inspection_date DATE NOT NULL, + inspection_interval_days INTEGER NOT NULL DEFAULT 30, + status TEXT NOT NULL, + notes TEXT NOT NULL DEFAULT '', + technician_name TEXT NOT NULL, + created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS log_files ( + id SERIAL PRIMARY KEY, + log_id INTEGER NOT NULL REFERENCES logs(id) ON DELETE CASCADE, + original_name TEXT NOT NULL, + stored_name TEXT NOT NULL, + size INTEGER NOT NULL, + mime_type TEXT NOT NULL + ); + `); + + await pool.query(` + ALTER TABLE logs + ADD COLUMN IF NOT EXISTS elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL; + `); +} + +async function migrateLegacyJsonIfNeeded() { + if (!fs.existsSync(LEGACY_DATA_FILE)) { + return; + } + + const userCount = await pool.query("SELECT COUNT(*)::int AS count FROM users"); + + if (userCount.rows[0].count > 0) { + return; + } + + const legacy = JSON.parse(fs.readFileSync(LEGACY_DATA_FILE, "utf-8")); + const client = await pool.connect(); + const userIdMap = new Map(); + + try { + await client.query("BEGIN"); + + for (const user of legacy.users || []) { + const result = await client.query( + `INSERT INTO users (username, password_hash, full_name, route_name, created_at) + VALUES ($1, $2, $3, $4, $5) + RETURNING id`, + [user.username, user.passwordHash, user.fullName, user.routeName, user.createdAt || new Date().toISOString()] + ); + userIdMap.set(user.id, result.rows[0].id); + } + + for (const elevator of legacy.elevators || []) { + await client.query( + `INSERT INTO elevators ( + name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [ + elevator.name, + elevator.serialNumber, + elevator.location, + elevator.routeName, + elevator.lastInspectionDate || null, + elevator.nextInspectionDate || null, + elevator.inspectionIntervalDays || 30, + elevator.lastStatus || "rendben", + elevator.updatedAt || new Date().toISOString() + ] + ); + } + + for (const log of legacy.logs || []) { + const logResult = await client.query( + `INSERT INTO logs ( + elevator_name, serial_number, location, route_name, inspection_date, + next_inspection_date, inspection_interval_days, status, notes, + technician_name, created_by_user_id, created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING id`, + [ + log.elevatorName, + log.serialNumber, + log.location, + log.routeName, + log.inspectionDate, + log.nextInspectionDate, + log.inspectionIntervalDays || 30, + log.status || "rendben", + log.notes || "", + log.technicianName, + userIdMap.get(log.createdByUserId) || null, + log.createdAt || new Date().toISOString() + ] + ); + + for (const file of log.files || []) { + await client.query( + `INSERT INTO log_files (log_id, original_name, stored_name, size, mime_type) + VALUES ($1, $2, $3, $4, $5)`, + [logResult.rows[0].id, file.originalName, file.storedName, file.size || 0, file.mimeType || "application/pdf"] + ); + } + } + + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } +} + +function createToken(user) { + return jwt.sign(user, JWT_SECRET, { expiresIn: "8h" }); +} + +function setAuthCookie(res, token) { + res.setHeader("Set-Cookie", serializeCookie(COOKIE_NAME, token, { + httpOnly: true, + sameSite: "Lax", + path: "/", + maxAge: 8 * 60 * 60 + })); +} + +function clearAuthCookie(res) { + res.setHeader("Set-Cookie", serializeCookie(COOKIE_NAME, "", { + httpOnly: true, + sameSite: "Lax", + path: "/", + maxAge: 0 + })); +} + +function serializeCookie(name, value, options) { + const parts = [`${name}=${encodeURIComponent(value)}`]; + parts.push(`Path=${options.path || "/"}`); + parts.push(`Max-Age=${options.maxAge || 0}`); + parts.push("HttpOnly"); + parts.push(`SameSite=${options.sameSite || "Lax"}`); + return parts.join("; "); +} + +function parseCookies(req) { + const rawCookie = req.headers.cookie || ""; + + return rawCookie.split(";").reduce((cookies, entry) => { + const [name, ...rest] = entry.trim().split("="); + + if (!name) { + return cookies; + } + + cookies[name] = decodeURIComponent(rest.join("=")); + return cookies; + }, {}); +} + +function getCookieToken(req) { + return parseCookies(req)[COOKIE_NAME] || ""; +} + +function getTokenFromRequest(req) { + const headerToken = req.headers.authorization?.startsWith("Bearer ") + ? req.headers.authorization.slice(7) + : ""; + const queryToken = req.query.token?.trim() || ""; + const cookieToken = getCookieToken(req); + + return headerToken || cookieToken || queryToken; +} + +function verifyToken(token) { + try { + return jwt.verify(token, JWT_SECRET); + } catch (_error) { + return null; + } +} + +function requireGuestPage(req, res, next) { + const token = getCookieToken(req); + return token && verifyToken(token) ? res.redirect("/alkalmazas") : next(); +} + +function requirePageAuth(req, res, next) { + const token = getCookieToken(req); + const decoded = token ? verifyToken(token) : null; + + if (!decoded) { + return res.redirect("/bejelentkezes"); + } + + req.user = decoded; + next(); +} + +function authenticateRequest(req, res, next) { + const token = getTokenFromRequest(req); + const decoded = token ? verifyToken(token) : null; + + if (!decoded) { + return res.status(401).json({ message: "Bejelentkezés szükséges." }); + } + + req.user = decoded; + next(); +} + +function normalizeElevatorPayload(body, user) { + const inspectionIntervalDays = Number(body.inspectionIntervalDays || 30); + + return { + name: body.name?.trim(), + serialNumber: body.serialNumber?.trim().toUpperCase(), + location: body.location?.trim(), + routeName: user.routeName, + lastInspectionDate: body.lastInspectionDate?.trim() || null, + nextInspectionDate: body.nextInspectionDate?.trim() || null, + inspectionIntervalDays: Number.isFinite(inspectionIntervalDays) ? inspectionIntervalDays : 30, + status: body.status?.trim() || "rendben" + }; +} + +function validateElevatorPayload(payload) { + if (!payload.name || !payload.serialNumber || !payload.location) { + return "Az eszköz neve, gyári száma és helyszíne kötelező."; + } + + if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) { + return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie."; + } + + if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) { + return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie."; + } + + return ""; +} + +function normalizeLogPayload(body, user) { + const inspectionIntervalDays = Number(body.inspectionIntervalDays || 30); + + return { + elevatorId: Number(body.elevatorId || 0) || null, + elevatorName: body.elevatorName?.trim(), + serialNumber: body.serialNumber?.trim().toUpperCase(), + location: body.location?.trim(), + routeName: body.routeName?.trim() || user.routeName, + inspectionDate: body.inspectionDate?.trim(), + nextInspectionDate: body.nextInspectionDate?.trim(), + inspectionIntervalDays: Number.isFinite(inspectionIntervalDays) ? inspectionIntervalDays : 30, + status: body.status?.trim() || "rendben", + notes: body.notes?.trim() || "" + }; +} + +function validateLogPayload(payload) { + if (!payload.elevatorId && (!payload.elevatorName || !payload.serialNumber || !payload.location)) { + return "A lift neve, gyári száma és helyszíne kötelező."; + } + + if (!payload.inspectionDate || !payload.nextInspectionDate) { + return "Az ellenőrzés és a következő ellenőrzés dátuma kötelező."; + } + + if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) { + return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie."; + } + + return ""; +} + +function removeUploadedFiles(files) { + files.forEach((file) => { + const absolutePath = path.join(UPLOAD_DIR, file.filename); + + if (fs.existsSync(absolutePath)) { + fs.unlinkSync(absolutePath); + } + }); +} + +function buildLogOrder(sortBy) { + const orderMap = { + inspectionDateAsc: "l.inspection_date ASC, l.id DESC", + inspectionDateDesc: "l.inspection_date DESC, l.id DESC", + nextInspectionAsc: "l.next_inspection_date ASC, l.id DESC", + nextInspectionDesc: "l.next_inspection_date DESC, l.id DESC", + elevatorNameAsc: "l.elevator_name ASC, l.id DESC", + statusAsc: "l.status ASC, l.id DESC" + }; + + return orderMap[sortBy] || orderMap.inspectionDateDesc; +} + +async function fetchLogById(logId) { + const result = await pool.query( + `SELECT l.id, l.elevator_id, l.elevator_name, l.serial_number, l.location, l.route_name, l.inspection_date, + l.next_inspection_date, l.inspection_interval_days, l.status, l.notes, + l.technician_name, l.created_by_user_id, l.created_at, + COALESCE( + JSON_AGG( + JSON_BUILD_OBJECT( + 'id', lf.id, + 'originalName', lf.original_name, + 'size', lf.size, + 'downloadPath', '/api/logs/' || l.id || '/files/' || lf.id + ) + ORDER BY lf.id + ) FILTER (WHERE lf.id IS NOT NULL), + '[]'::json + ) AS files + FROM logs l + LEFT JOIN log_files lf ON lf.log_id = l.id + WHERE l.id = $1 + GROUP BY l.id`, + [logId] + ); + + return result.rowCount ? mapLogRow(result.rows[0]) : null; +} + +async function findElevatorById(executor, elevatorId, routeName) { + const result = await executor.query( + `SELECT id, name, serial_number, location, route_name, last_inspection_date, + next_inspection_date, inspection_interval_days, last_status, updated_at + FROM elevators + WHERE id = $1 AND route_name = $2`, + [elevatorId, routeName] + ); + + return result.rowCount ? result.rows[0] : null; +} + +function mapUserRow(row) { + return { + id: row.id, + username: row.username, + fullName: row.full_name, + routeName: row.route_name + }; +} + +function mapElevatorRow(row) { + return { + id: row.id, + name: row.name, + serialNumber: row.serial_number, + location: row.location, + routeName: row.route_name, + lastInspectionDate: row.last_inspection_date, + nextInspectionDate: row.next_inspection_date, + inspectionIntervalDays: row.inspection_interval_days, + lastStatus: row.last_status, + updatedAt: row.updated_at + }; +} + +function mapLogRow(row) { + return { + id: row.id, + elevatorId: row.elevator_id, + elevatorName: row.elevator_name, + serialNumber: row.serial_number, + location: row.location, + routeName: row.route_name, + inspectionDate: row.inspection_date, + nextInspectionDate: row.next_inspection_date, + inspectionIntervalDays: row.inspection_interval_days, + status: row.status, + notes: row.notes, + technicianName: row.technician_name, + createdByUserId: row.created_by_user_id, + createdAt: row.created_at, + files: Array.isArray(row.files) + ? row.files.map((file) => ({ + ...file, + originalName: normalizeUploadedFilename(file.originalName) + })) + : [] + }; +} + +function normalizeUploadedFilename(value) { + if (!value || !containsLikelyFilenameMojibake(value)) { + return value; + } + + const decoded = Buffer.from(value, "latin1").toString("utf8").normalize("NFC"); + return containsReplacementCharacter(decoded) ? value : decoded; +} + +function containsLikelyFilenameMojibake(value) { + return /[\u00C2-\u00C5\u00CC-\u00CF\u00D0-\u00D6\u00D8-\u00DE\u0080-\u009F]/.test(value); + return /[ÃÅÂ]/.test(value); +} + +function containsReplacementCharacter(value) { + return value.includes("\uFFFD"); +}