diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 546e10d..e37cd5e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,9 @@ jobs: - name: Install dependencies run: npm install --include=dev + - name: Run tests + run: npm run test + - name: Run semantic-release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -35,4 +38,4 @@ jobs: REPO_OWNER: "Local-Connectivity-Lab" REPO_NAME: "ccn-coverage-docker" TARGET_ARTIFACT_NAME: "ccn-coverage-api" - run: npx semantic-release + run: npx semantic-release \ No newline at end of file diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index 4ba00ed..641dd38 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -2,7 +2,7 @@ services: vis: container_name: vis - image: ghcr.io/local-connectivity-lab/ccn-coverage-vis:latest + image: ccn-coverage-vis:latest ports: - "8090:80" depends_on: @@ -12,7 +12,7 @@ services: api: container_name: api - image: ghcr.io/local-connectivity-lab/ccn-coverage-api:latest + image: ccn-coverage-api:latest ports: - "8091:3000" environment: diff --git a/docker-compose.yaml b/docker-compose.yaml index 473518f..a8486f1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: - MONGODB_URI=mongodb://mongodb:27017/api-data - LDAP_URI=ldap://ldap:389 volumes: - - ./keys/:/app/keys + - ./keys/:/usr/src/app/keys/ depends_on: - mongodb - ldap diff --git a/package-lock.json b/package-lock.json index 39612f7..9730b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,12 +38,16 @@ "@types/express": "^5.0.0", "@types/express-session": "^1.18.1", "@types/leaflet": "^1.7.5", + "@types/mocha": "^10.0.10", "@types/passport": "^1.0.17", "copyfiles": "^2.4.1", "js-yaml": "^4.1.0", "jsdom-global": "3.0.2", + "mocha": "^11.7.1", + "node-mocks-http": "^1.17.2", "prettier": "^3.5.3", "semantic-release": "^24.2.4", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" } }, @@ -219,6 +223,102 @@ "node": ">=18" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -529,6 +629,16 @@ "@octokit/openapi-types": "^25.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -1559,6 +1669,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "node_modules/@types/node": { "version": "22.13.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", @@ -1947,6 +2063,12 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "node_modules/bson": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", @@ -2011,6 +2133,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -2487,6 +2621,18 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decimal.js": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", @@ -2641,6 +2787,12 @@ "xtend": "^4.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3181,6 +3333,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -3201,6 +3362,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -3601,6 +3778,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -4085,6 +4271,21 @@ "node": "^18.17 || >=20.6.1" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -4356,6 +4557,50 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -4572,6 +4817,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4585,6 +4839,295 @@ "node": ">=10" } }, + "node_modules/mocha": { + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/mongodb": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.1.tgz", @@ -4770,6 +5313,48 @@ "node": ">=18" } }, + "node_modules/node-mocks-http": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.17.2.tgz", + "integrity": "sha512-HVxSnjNzE9NzoWMx9T9z4MLqwMpLwVvA0oVZ+L+gXskYXEJ6tFn3Kx4LargoB6ie7ZlCLplv7QbWO6N+MysWGA==", + "dev": true, + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/node-mocks-http/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", @@ -7816,6 +8401,12 @@ "node": ">=4" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7974,6 +8565,22 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -8217,6 +8824,15 @@ "node": ">= 0.8" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8767,6 +9383,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -9213,6 +9838,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "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==", + "dev": true, + "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", @@ -9226,6 +9866,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -9571,7 +10224,6 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9960,6 +10612,12 @@ "dev": true, "license": "MIT" }, + "node_modules/workerpool": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "dev": true + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9978,6 +10636,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -10070,6 +10746,30 @@ "node": ">=10" } }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -10080,6 +10780,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", diff --git a/package.json b/package.json index 7d2b25c..7fa6532 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,16 @@ "@types/express": "^5.0.0", "@types/express-session": "^1.18.1", "@types/leaflet": "^1.7.5", + "@types/mocha": "^10.0.10", "@types/passport": "^1.0.17", "copyfiles": "^2.4.1", "js-yaml": "^4.1.0", "jsdom-global": "3.0.2", + "mocha": "^11.7.1", + "node-mocks-http": "^1.17.2", "prettier": "^3.5.3", "semantic-release": "^24.2.4", + "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0" }, "dependencies": { @@ -46,6 +50,7 @@ "build": "tsc -p tsconfig.json && copyfiles -f ./src/models/*.json ./build/src/models", "start-built": "node ./build/src/index.js", "format": "prettier --write \"src/**/*.{ts,tsx,json}\"", - "format:check": "prettier --check \"src/**/*.{ts,tsx,json}\"" + "format:check": "prettier --check \"src/**/*.{ts,tsx,json}\"", + "test": "mocha -r ts-node/register 'src/test/**/*.test.ts'" } } diff --git a/src/index.ts b/src/index.ts index 4848d3d..77f09b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,8 @@ import { ldapRouter } from './routes/ldap-login'; import { newUserRouter } from './routes/new-user'; import { usersRouter } from './routes/users'; import { editSitesRouter } from './routes/edit-sites'; +import { secureSitesRouter } from './routes/secure-site'; +import { publicSitesRouter } from './routes/public-sites'; import logger from './logger'; import cors from 'cors'; @@ -72,6 +74,8 @@ app.use(ldapRouter); app.use(newUserRouter); app.use(usersRouter); app.use(editSitesRouter); +app.use(secureSitesRouter); +app.use(publicSitesRouter); process.on('SIGINT', async () => { await mongoose.connection.close(); diff --git a/src/models/measurement.ts b/src/models/measurement.ts index bb47c22..a9f910a 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -46,5 +46,4 @@ const MeasurementData = mongoose.model< MeasurementModelInterface >('Measurement', measurementSchema); - export { MeasurementData, IMeasurement, MeasurementDoc }; diff --git a/src/models/site.ts b/src/models/site.ts new file mode 100644 index 0000000..53ad7ad --- /dev/null +++ b/src/models/site.ts @@ -0,0 +1,82 @@ +import mongoose from 'mongoose'; +import { components } from '../types/schema'; + +type ISite = components['schemas']['Site']; + +interface SiteDoc extends mongoose.Document, ISite {} + +interface SiteModelInterface extends mongoose.Model { + build(attr: ISite): SiteDoc; +} + +const siteSchema = new mongoose.Schema( + { + identity: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + latitude: { + type: Number, + required: true, + validate: { + validator: function (latitude: number) { + return latitude >= -90 && latitude <= 90; + }, + message: 'Latitude must be between -90 and 90 degrees', + }, + }, + longitude: { + type: Number, + required: true, + validate: { + validator: function (longitude: number) { + return longitude >= -180 && longitude <= 180; + }, + message: 'Longitude must be between -180 and 180 degrees', + }, + }, + status: { + type: String, + enum: ['active', 'confirmed', 'in-conversation'], + required: true, + }, + address: { + type: String, + required: true, + }, + cell_id: { + type: [String], + required: true, + }, + color: { + type: String, + required: false, + }, + boundary: { + type: [[Number]], + validate: { + validator: function (val: number[][]) { + return val.every(pair => pair.length === 2); + }, + message: + 'Each boundary coordinate must be a pair of [latitude, longitude]', + }, + required: false, + }, + }, + { + versionKey: false, + }, +); + +siteSchema.statics.build = (attr: ISite) => { + return new Site(attr); +}; + +const Site = mongoose.model('Site', siteSchema); + +export { Site, ISite, SiteDoc }; diff --git a/src/models/user.ts b/src/models/user.ts index a86c5f9..eca0349 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -65,5 +65,4 @@ userSchema.statics.build = (attr: IUser) => { const User = mongoose.model('User', userSchema); - export { User, IUser, UserDoc }; diff --git a/src/routes/public-sites.ts b/src/routes/public-sites.ts new file mode 100644 index 0000000..839cb7a --- /dev/null +++ b/src/routes/public-sites.ts @@ -0,0 +1,18 @@ +import express, { Request, Response } from 'express'; +import { Site } from '../models/site'; + +const router = express.Router(); + +// Get list of all sites from database +router.get('/api/sites', async (req: Request, res: Response) => { + try { + const sites = await Site.find(); + + res.status(200).json(sites); + } catch (error) { + console.error('Error retrieving public sites:', error); + res.status(500).send('Internal server error'); + } +}); + +export { router as publicSitesRouter }; diff --git a/src/routes/query.ts b/src/routes/query.ts index 708ec4c..906dee3 100644 --- a/src/routes/query.ts +++ b/src/routes/query.ts @@ -67,10 +67,6 @@ function updateSite() { return JSON.parse(fs.readFileSync(defaultModelJsonPath).toString()); } } -router.get('/api/sites', (_, res: Response) => { - sites = updateSite(); - res.send(sites); -}); router.get('/api/dataRange', (_, res) => { res.send(dataRange); diff --git a/src/routes/report.ts b/src/routes/report.ts index f153a00..8c522f9 100644 --- a/src/routes/report.ts +++ b/src/routes/report.ts @@ -11,7 +11,7 @@ function runIfAuthenticated(req: Request, res: Response, next: any) { const signature = Buffer.from(req.body.sigma_m, 'hex'); const message = Buffer.from(req.body.M, 'hex'); User.findOne({ identity: req.body.h_pkr.toLowerCase() }) - .then((user) => { + .then(user => { if (!user) { res.status(401).send('user not found'); logger.debug('User not found'); diff --git a/src/routes/secure-site.ts b/src/routes/secure-site.ts new file mode 100644 index 0000000..7091fcf --- /dev/null +++ b/src/routes/secure-site.ts @@ -0,0 +1,106 @@ +import express, { Request, Response } from 'express'; +import { Site } from '../models/site'; +import { components } from '../types/schema'; +import connectEnsureLogin from 'connect-ensure-login'; +import * as crypto from 'crypto'; + +const router = express.Router(); + +type EditSiteRequest = components['schemas']['Site']; +type NewSiteRequest = components['schemas']['NewSiteRequest']; + +// Edit an existing site +export const putSecureSite = async (req: Request, res: Response) => { + try { + const siteData: EditSiteRequest = req.body; + + if (!siteData.identity) { + res.status(400).json({ error: 'Site identity is required' }); + return; + } + + const updatedSite = await Site.findOneAndUpdate( + { identity: siteData.identity }, + siteData, + { new: true, runValidators: true }, + ); + + if (!updatedSite) { + res.status(404).json({ error: 'Site not found' }); + return; + } + + res.status(200).json({ message: 'Site updated successfully' }); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Create a new site +export const postSecureSite = async (req: Request, res: Response) => { + try { + const siteData: NewSiteRequest = req.body; + const identity = crypto.randomUUID(); + + const siteWithIdentity: EditSiteRequest = { + identity, + ...siteData, + }; + + const newSite = Site.build(siteWithIdentity); + const savedSite = await newSite.save(); + + res.status(201).json({ message: 'Site created successfully', identity }); + } catch (error: any) { + if (error.name === 'ValidationError') { + res + .status(400) + .json({ error: 'Validation error', details: error.message }); + return; + } + res.status(500).json({ error: 'Internal server error' }); + } +}; + +// Delete an existing site +export const deleteSecureSite = async (req: Request, res: Response) => { + try { + const { identity } = req.body; + + if (!identity) { + res.status(400).json({ error: 'Site identity is required' }); + return; + } + + const deletedSite = await Site.findOneAndDelete({ identity }); + + if (!deletedSite) { + res.status(404).json({ error: 'Site not found' }); + return; + } + + res + .status(200) + .json({ message: 'Site deleted successfully', site: deletedSite }); + } catch (error: any) { + res.status(500).json({ error: 'Internal server error' }); + } +}; + +router.put( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + putSecureSite, +); +router.post( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + postSecureSite, +); +router.delete( + '/secure/edit-sites', + connectEnsureLogin.ensureLoggedIn(), + deleteSecureSite, +); + +export { router as secureSitesRouter }; diff --git a/src/test/secure-site.test.ts b/src/test/secure-site.test.ts new file mode 100644 index 0000000..46e8850 --- /dev/null +++ b/src/test/secure-site.test.ts @@ -0,0 +1,213 @@ +import * as assert from 'assert'; +import * as httpMocks from 'node-mocks-http'; +import { + putSecureSite, + postSecureSite, + deleteSecureSite, +} from '../routes/secure-site'; +import { Site } from '../models/site'; + +describe('secure-site event handlers', function () { + describe('PUT /api/secure-site', function () { + it('400 if identity missing', async function () { + const req = httpMocks.createRequest({ method: 'PUT', body: {} }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Site identity is required', + }); + }); + + it('404 if site not found', async function () { + (Site.findOneAndUpdate as any) = async () => null; + + const req = httpMocks.createRequest({ + method: 'PUT', + body: { identity: 'test-identity' }, + }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 404); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site not found' }); + }); + + it('200 on success', async function () { + (Site.findOneAndUpdate as any) = async () => ({ name: 'X' }); + + const req = httpMocks.createRequest({ + method: 'PUT', + body: { identity: 'test-identity', name: 'X' }, + }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 200); + assert.deepStrictEqual(res._getJSONData(), { + message: 'Site updated successfully', + }); + }); + + it('500 on error', async function () { + (Site.findOneAndUpdate as any) = async () => { + throw new Error('fail'); + }; + + const req = httpMocks.createRequest({ + method: 'PUT', + body: { identity: 'test-identity', name: 'X' }, + }); + const res = httpMocks.createResponse(); + + await putSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); + }); + }); + + describe('POST /api/secure-site', function () { + it('201 on success', async function () { + (Site.build as any) = () => ({ save: async () => ({}) }); + + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'New' }, + }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 201); + // The actual identity value will be a SHA256 hash of the name + const response = res._getJSONData(); + assert.strictEqual(response.message, 'Site created successfully'); + assert.ok(response.identity && typeof response.identity === 'string'); + }); + + it('400 on validation error', async function () { + (Site.build as any) = () => ({ + save: async () => { + throw { name: 'ValidationError', message: 'Bad' }; + }, + }); + + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'Invalid' }, + }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Validation error', + details: 'Bad', + }); + }); + + it('500 on other error', async function () { + (Site.build as any) = () => ({ + save: async () => { + throw new Error('fail'); + }, + }); + + const req = httpMocks.createRequest({ + method: 'POST', + body: { name: 'X' }, + }); + const res = httpMocks.createResponse(); + + await postSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); + }); + }); + + describe('DELETE /api/secure-site', function () { + it('400 if identity missing', async function () { + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { identity: '' }, + }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 400); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Site identity is required', + }); + }); + + it('404 if site not found', async function () { + (Site.findOneAndDelete as any) = async () => null; + + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { identity: 'test-identity' }, + }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 404); + assert.deepStrictEqual(res._getJSONData(), { error: 'Site not found' }); + }); + + it('200 on success', async function () { + (Site.findOneAndDelete as any) = async () => ({ + identity: 'test-identity', + name: 'X', + }); + + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { identity: 'test-identity' }, + }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 200); + assert.deepStrictEqual(res._getJSONData(), { + message: 'Site deleted successfully', + site: { + identity: 'test-identity', + name: 'X', + }, + }); + }); + + it('500 on error', async function () { + (Site.findOneAndDelete as any) = async () => { + throw new Error('fail'); + }; + + const req = httpMocks.createRequest({ + method: 'DELETE', + body: { identity: 'test-identity' }, + }); + const res = httpMocks.createResponse(); + + await deleteSecureSite(req, res); + + assert.strictEqual(res._getStatusCode(), 500); + assert.deepStrictEqual(res._getJSONData(), { + error: 'Internal server error', + }); + }); + }); +}); diff --git a/src/types/schema.d.ts b/src/types/schema.d.ts index 07f96f8..782aae1 100644 --- a/src/types/schema.d.ts +++ b/src/types/schema.d.ts @@ -618,7 +618,6 @@ export interface paths { * @description Parses CSV data and stores it as both signal and measurement records. * If a group is specified, any existing data with that group will be removed first. * The CSV should include columns for date, time, coordinate, cell_id, dbm, ping, download_speed, and upload_speed. - * */ post: { parameters: { @@ -698,7 +697,6 @@ export interface paths { * @description Returns two lists: * 1. Registered users sorted by issue date (newest first) * 2. Pending users whose issue date is within the expiry display limit, sorted by issue date (newest first) - * */ post: { parameters: { @@ -825,7 +823,6 @@ export interface paths { * Uses Passport LDAP strategy which binds to the LDAP server with the provided credentials. * On success, creates a session and redirects to /api/success. * On failure, redirects to /api/failure. - * */ post: { parameters: { @@ -937,7 +934,6 @@ export interface paths { * Update site configuration * @description Updates the sites configuration file with provided data. * Requires user to be authenticated - will redirect to login page if not authenticated. - * */ post: { parameters: { @@ -1016,7 +1012,6 @@ export interface paths { * @description Creates a new user with a cryptographically secure identity using EC keys. * Generates keypairs, creates signatures, and stores user information. * Requires authentication - will redirect to login page if not authenticated. - * */ post: { parameters: { @@ -1075,6 +1070,190 @@ export interface paths { patch?: never; trace?: never; }; + '/secure/edit-sites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Update an existing site + * @description Updates an existing site with the provided information + */ + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['Site']; + }; + }; + responses: { + /** @description Site successfully updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while updating site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Add a new site + * @description Creates a new site with the provided information + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['NewSiteRequest']; + }; + }; + responses: { + /** @description Site successfully created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while creating site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + /** + * Delete a site + * @description Removes an existing site from the system using its unique identity + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** + * @description The unique identifier of the site to delete + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; + }; + }; + }; + responses: { + /** @description Site successfully deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Bad request - invalid site data */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Unauthorized - User not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + /** @description Server error while deleting site */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/plain': string; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1271,6 +1450,11 @@ export interface components { package_loss: number; }; Site: { + /** + * @description Unique identifier of the user to update + * @example 9a8b7c6d5e4f3g2h1i + */ + identity: string; /** * @description Name of the site * @example Filipino Community Center @@ -1309,14 +1493,55 @@ export interface components { /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ boundary?: [number, number][]; }; - /** @example { + NewSiteRequest: { + /** + * @description Name of the site + * @example Filipino Community Center + */ + name: string; + /** + * Format: double + * @description Geographic latitude coordinate + * @example 47.681932654395915 + */ + latitude: number; + /** + * Format: double + * @description Geographic longitude coordinate + * @example -122.31829217664796 + */ + longitude: number; + /** + * @description Current status of the site + * @example active + * @enum {string} + */ + status: NewSiteRequest; + /** + * @description Physical address of the site + * @example 5740 Martin Luther King Jr Way S, Seattle, WA 98118 + */ + address: string; + /** @description Array of cell identifiers associated with the site */ + cell_id: string[]; + /** + * @description Optional color identifier for the site in hex code + * @example #FF5733 + */ + color?: string; + /** @description Optional geographical boundary coordinates defining the site perimeter as [latitude, longitude] pairs */ + boundary?: [number, number][]; + }; + /** + * @example { * "Filipino Community Center": { * "ping": 115.28, * "download_speed": 7.16, * "upload_speed": 8.63, * "dbm": -78.4 * } - * } */ + * } + */ SitesSummary: { [key: string]: { /** @@ -1737,3 +1962,8 @@ export enum SiteStatus { confirmed = 'confirmed', in_conversation = 'in-conversation', } +export enum NewSiteRequestStatus { + active = 'active', + confirmed = 'confirmed', + in_conversation = 'in-conversation', +}