Add comprehensive OpenAPI/Swagger API documentation
- Install swagger-ui-express and swagger-jsdoc dependencies - Create comprehensive OpenAPI 3.0 specification with detailed schemas - Add interactive Swagger UI at /api-docs endpoint - Document all public API endpoints (/api/config, /api/locations) - Document admin authentication and management endpoints - Include comprehensive request/response schemas and examples - Add authentication documentation for admin endpoints - Update CLAUDE.md with API documentation information Features: - Complete API specification with OpenAPI 3.0 standard - Interactive documentation interface with Swagger UI - Detailed request/response examples for all endpoints - Authentication flows for admin functionality - Error response documentation with examples - Type-safe integration with existing TypeScript architecture API Documentation available at: /api-docs 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
13c0b8b457
commit
612475727e
8 changed files with 906 additions and 14 deletions
11
CLAUDE.md
11
CLAUDE.md
|
@ -21,6 +21,17 @@ npm run dev-with-css # Legacy JS + CSS watching
|
||||||
|
|
||||||
The application runs on port 3000 by default. Visit http://localhost:3000 to view the website.
|
The application runs on port 3000 by default. Visit http://localhost:3000 to view the website.
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
Interactive OpenAPI/Swagger documentation is available at `/api-docs` when the server is running:
|
||||||
|
- **Development**: http://localhost:3000/api-docs
|
||||||
|
- **Production**: https://yourapp.com/api-docs
|
||||||
|
|
||||||
|
The documentation includes:
|
||||||
|
- Complete API endpoint specifications
|
||||||
|
- Request/response schemas and examples
|
||||||
|
- Authentication requirements
|
||||||
|
- Interactive testing interface
|
||||||
|
|
||||||
### TypeScript Development
|
### TypeScript Development
|
||||||
The backend is written in TypeScript and compiles to `dist/` directory.
|
The backend is written in TypeScript and compiles to `dist/` directory.
|
||||||
```bash
|
```bash
|
||||||
|
|
300
package-lock.json
generated
300
package-lock.json
generated
|
@ -14,7 +14,9 @@
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"sqlite3": "^5.1.6"
|
"sqlite3": "^5.1.6",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
|
@ -22,6 +24,8 @@
|
||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
@ -45,6 +49,68 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"@types/json-schema": "^7.0.6",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "^9.0.6",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.0.4",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"@jsdevtools/ono": "^7.1.3",
|
||||||
|
"call-me-maybe": "^1.0.1",
|
||||||
|
"z-schema": "^5.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
|
@ -1017,6 +1083,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jsdevtools/ono": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
|
@ -1390,6 +1462,13 @@
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
|
@ -1600,6 +1679,12 @@
|
||||||
"@types/istanbul-lib-report": "*"
|
"@types/istanbul-lib-report": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
|
@ -1678,6 +1763,24 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/swagger-jsdoc": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/swagger-ui-express": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/serve-static": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.33",
|
"version": "17.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||||
|
@ -2058,7 +2161,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
|
@ -2142,7 +2244,6 @@
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
|
@ -2304,6 +2405,12 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
@ -2538,6 +2645,15 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/component-emitter": {
|
"node_modules/component-emitter": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||||
|
@ -2552,7 +2668,6 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/concurrently": {
|
"node_modules/concurrently": {
|
||||||
|
@ -2865,6 +2980,18 @@
|
||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/doctrine": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"esutils": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.0.1",
|
"version": "17.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz",
|
||||||
|
@ -3078,6 +3205,15 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/etag": {
|
"node_modules/etag": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
@ -3340,7 +3476,6 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
|
@ -3797,7 +3932,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
"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.",
|
"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.",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.3.0",
|
"once": "^1.3.0",
|
||||||
|
@ -4788,6 +4922,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.mergewith": {
|
||||||
|
"version": "4.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
@ -4978,7 +5132,6 @@
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
|
@ -5374,6 +5527,13 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
|
@ -5487,7 +5647,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
@ -6574,6 +6733,83 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-jsdoc": {
|
||||||
|
"version": "6.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "6.2.0",
|
||||||
|
"doctrine": "3.0.0",
|
||||||
|
"glob": "7.1.6",
|
||||||
|
"lodash.mergewith": "^4.6.2",
|
||||||
|
"swagger-parser": "^10.0.3",
|
||||||
|
"yaml": "2.0.0-1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"swagger-jsdoc": "bin/swagger-jsdoc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-jsdoc/node_modules/glob": {
|
||||||
|
"version": "7.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||||
|
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-parser": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "10.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.26.0.tgz",
|
||||||
|
"integrity": "sha512-U8m1LruHrk33gIIT5qDKhXMygT4FonRGBE92zMbxP4i9ULolPlKISy5Pd3RCES8pWdbGzXhvm/Q6jdA/HsrClg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
|
@ -6931,6 +7167,15 @@
|
||||||
"node": ">=10.12.0"
|
"node": ">=10.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validator": {
|
||||||
|
"version": "13.15.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||||
|
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
@ -7030,6 +7275,15 @@
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.0.0-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
|
||||||
|
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
@ -7081,6 +7335,36 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/z-schema": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"validator": "^13.7.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"z-schema": "bin/z-schema"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"commander": "^9.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/z-schema/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,9 @@
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"sqlite3": "^5.1.6"
|
"sqlite3": "^5.1.6",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
|
@ -32,6 +34,8 @@
|
||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
|
|
@ -79,7 +79,41 @@ export default (
|
||||||
): Router => {
|
): Router => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Admin login
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/admin/login:
|
||||||
|
* post:
|
||||||
|
* tags:
|
||||||
|
* - Admin - Authentication
|
||||||
|
* summary: Admin login
|
||||||
|
* description: Authenticate as administrator to access protected admin endpoints
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/AdminLoginRequest'
|
||||||
|
* example:
|
||||||
|
* password: "your_admin_password"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Login successful
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/AdminLoginResponse'
|
||||||
|
* example:
|
||||||
|
* token: "admin_token_here"
|
||||||
|
* message: "Login successful"
|
||||||
|
* 401:
|
||||||
|
* description: Invalid credentials
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* example:
|
||||||
|
* error: "Invalid password"
|
||||||
|
*/
|
||||||
router.post('/login', (req: AdminLoginRequest, res: Response): void => {
|
router.post('/login', (req: AdminLoginRequest, res: Response): void => {
|
||||||
console.log('Admin login attempt');
|
console.log('Admin login attempt');
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
@ -94,7 +128,61 @@ export default (
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all locations for admin (including expired ones)
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/admin/locations:
|
||||||
|
* get:
|
||||||
|
* tags:
|
||||||
|
* - Admin - Locations
|
||||||
|
* summary: Get all location reports (including expired)
|
||||||
|
* description: Retrieve all location reports for admin management, including expired and persistent reports
|
||||||
|
* security:
|
||||||
|
* - BearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: All locations retrieved successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* allOf:
|
||||||
|
* - $ref: '#/components/schemas/Location'
|
||||||
|
* - type: object
|
||||||
|
* properties:
|
||||||
|
* isActive:
|
||||||
|
* type: boolean
|
||||||
|
* description: Whether the report is still active (< 48 hours)
|
||||||
|
* example:
|
||||||
|
* - id: 123
|
||||||
|
* address: "Main St & Oak Ave, Grand Rapids, MI"
|
||||||
|
* description: "Black ice present"
|
||||||
|
* latitude: 42.9634
|
||||||
|
* longitude: -85.6681
|
||||||
|
* persistent: false
|
||||||
|
* created_at: "2025-01-15T10:30:00.000Z"
|
||||||
|
* isActive: true
|
||||||
|
* - id: 122
|
||||||
|
* address: "Old expired report"
|
||||||
|
* description: "No longer relevant"
|
||||||
|
* latitude: 42.9500
|
||||||
|
* longitude: -85.6700
|
||||||
|
* persistent: false
|
||||||
|
* created_at: "2025-01-10T08:00:00.000Z"
|
||||||
|
* isActive: false
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized - admin authentication required
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
router.get('/locations', authenticateAdmin, async (req: Request, res: Response): Promise<void> => {
|
router.get('/locations', authenticateAdmin, async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const rows = await locationModel.getAll();
|
const rows = await locationModel.getAll();
|
||||||
|
|
|
@ -3,7 +3,39 @@ import express, { Request, Response, Router } from 'express';
|
||||||
export default (): Router => {
|
export default (): Router => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Get API configuration
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/config:
|
||||||
|
* get:
|
||||||
|
* tags:
|
||||||
|
* - Public API
|
||||||
|
* summary: Get API configuration
|
||||||
|
* description: Returns public API configuration including MapBox access token for geocoding
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: API configuration retrieved successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ApiConfig'
|
||||||
|
* examples:
|
||||||
|
* with_mapbox:
|
||||||
|
* summary: Configuration with MapBox token
|
||||||
|
* value:
|
||||||
|
* mapboxAccessToken: "pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJhYmNkZWZnIn0.example"
|
||||||
|
* hasMapbox: true
|
||||||
|
* without_mapbox:
|
||||||
|
* summary: Configuration without MapBox token
|
||||||
|
* value:
|
||||||
|
* mapboxAccessToken: null
|
||||||
|
* hasMapbox: false
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
router.get('/', (req: Request, res: Response): void => {
|
router.get('/', (req: Request, res: Response): void => {
|
||||||
console.log('📡 API Config requested');
|
console.log('📡 API Config requested');
|
||||||
const MAPBOX_ACCESS_TOKEN: string | undefined = process.env.MAPBOX_ACCESS_TOKEN || undefined;
|
const MAPBOX_ACCESS_TOKEN: string | undefined = process.env.MAPBOX_ACCESS_TOKEN || undefined;
|
||||||
|
|
|
@ -22,7 +22,51 @@ interface LocationDeleteRequest extends Request {
|
||||||
export default (locationModel: Location, profanityFilter: ProfanityFilterService | any): Router => {
|
export default (locationModel: Location, profanityFilter: ProfanityFilterService | any): Router => {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Get all active locations (within 48 hours OR persistent)
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/locations:
|
||||||
|
* get:
|
||||||
|
* tags:
|
||||||
|
* - Public API
|
||||||
|
* summary: Get active ice condition reports
|
||||||
|
* description: |
|
||||||
|
* Retrieves all active ice condition reports. Reports are considered active if they are:
|
||||||
|
* - Less than 48 hours old, OR
|
||||||
|
* - Marked as persistent by an administrator
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Active locations retrieved successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Location'
|
||||||
|
* examples:
|
||||||
|
* active_locations:
|
||||||
|
* summary: Example active locations
|
||||||
|
* value:
|
||||||
|
* - id: 123
|
||||||
|
* address: "Main St & Oak Ave, Grand Rapids, MI"
|
||||||
|
* latitude: 42.9634
|
||||||
|
* longitude: -85.6681
|
||||||
|
* description: "Black ice present, multiple vehicles stuck"
|
||||||
|
* persistent: false
|
||||||
|
* created_at: "2025-01-15T10:30:00.000Z"
|
||||||
|
* - id: 124
|
||||||
|
* address: "I-96 & US-131, Grand Rapids, MI"
|
||||||
|
* latitude: 42.9584
|
||||||
|
* longitude: -85.6706
|
||||||
|
* description: "Icy on-ramp conditions"
|
||||||
|
* persistent: true
|
||||||
|
* created_at: "2025-01-14T08:15:00.000Z"
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
router.get('/', async (req: Request, res: Response): Promise<void> => {
|
router.get('/', async (req: Request, res: Response): Promise<void> => {
|
||||||
console.log('Fetching active locations');
|
console.log('Fetching active locations');
|
||||||
|
|
||||||
|
@ -36,7 +80,80 @@ export default (locationModel: Location, profanityFilter: ProfanityFilterService
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a new location
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/locations:
|
||||||
|
* post:
|
||||||
|
* tags:
|
||||||
|
* - Public API
|
||||||
|
* summary: Report a new ice condition
|
||||||
|
* description: |
|
||||||
|
* Submit a new ice condition report. The system will:
|
||||||
|
* - Validate the address is provided
|
||||||
|
* - Check description for profanity and reject if found
|
||||||
|
* - Geocode the address if coordinates not provided
|
||||||
|
* - Store the report for 48 hours (unless made persistent by admin)
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LocationInput'
|
||||||
|
* examples:
|
||||||
|
* basic_report:
|
||||||
|
* summary: Basic ice report with address only
|
||||||
|
* value:
|
||||||
|
* address: "Main St & Oak Ave, Grand Rapids, MI"
|
||||||
|
* description: "Black ice spotted, drive carefully"
|
||||||
|
* detailed_report:
|
||||||
|
* summary: Detailed report with coordinates
|
||||||
|
* value:
|
||||||
|
* address: "I-96 westbound at Exit 85"
|
||||||
|
* latitude: 42.9634
|
||||||
|
* longitude: -85.6681
|
||||||
|
* description: "Multiple vehicles stuck, road salt needed"
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Location report created successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Location'
|
||||||
|
* example:
|
||||||
|
* id: 125
|
||||||
|
* address: "Main St & Oak Ave, Grand Rapids, MI"
|
||||||
|
* latitude: 42.9634
|
||||||
|
* longitude: -85.6681
|
||||||
|
* description: "Black ice spotted, drive carefully"
|
||||||
|
* persistent: false
|
||||||
|
* created_at: "2025-01-15T12:00:00.000Z"
|
||||||
|
* 400:
|
||||||
|
* description: Bad request - invalid input or profanity detected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* examples:
|
||||||
|
* missing_address:
|
||||||
|
* summary: Missing required address field
|
||||||
|
* value:
|
||||||
|
* error: "Address is required"
|
||||||
|
* profanity_detected:
|
||||||
|
* summary: Profanity detected in description
|
||||||
|
* value:
|
||||||
|
* error: "Submission rejected"
|
||||||
|
* message: "Your description contains inappropriate language and cannot be posted. Please revise your description to focus on road conditions and keep it professional."
|
||||||
|
* details:
|
||||||
|
* severity: "medium"
|
||||||
|
* wordCount: 1
|
||||||
|
* detectedCategories: ["general"]
|
||||||
|
* 500:
|
||||||
|
* description: Internal server error
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
router.post('/', async (req: LocationPostRequest, res: Response): Promise<void> => {
|
router.post('/', async (req: LocationPostRequest, res: Response): Promise<void> => {
|
||||||
const { address, latitude, longitude } = req.body;
|
const { address, latitude, longitude } = req.body;
|
||||||
let { description } = req.body;
|
let { description } = req.body;
|
||||||
|
|
|
@ -3,6 +3,8 @@ import express, { Request, Response, NextFunction, Application } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
|
import swaggerUi from 'swagger-ui-express';
|
||||||
|
import { swaggerSpec } from './swagger';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config({ path: '.env.local' });
|
dotenv.config({ path: '.env.local' });
|
||||||
|
@ -167,6 +169,12 @@ function setupRoutes(): void {
|
||||||
const locationModel = databaseService.getLocationModel();
|
const locationModel = databaseService.getLocationModel();
|
||||||
const profanityWordModel = databaseService.getProfanityWordModel();
|
const profanityWordModel = databaseService.getProfanityWordModel();
|
||||||
|
|
||||||
|
// API Documentation
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
||||||
|
customCss: '.swagger-ui .topbar { display: none }',
|
||||||
|
customSiteTitle: 'Great Lakes Ice Report API Documentation'
|
||||||
|
}));
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
app.use('/api/config', configRoutes());
|
app.use('/api/config', configRoutes());
|
||||||
app.use('/api/locations', locationRoutes(locationModel, profanityFilter));
|
app.use('/api/locations', locationRoutes(locationModel, profanityFilter));
|
||||||
|
|
348
src/swagger.ts
Normal file
348
src/swagger.ts
Normal file
|
@ -0,0 +1,348 @@
|
||||||
|
import swaggerJsdoc from 'swagger-jsdoc';
|
||||||
|
|
||||||
|
const options: swaggerJsdoc.Options = {
|
||||||
|
definition: {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Great Lakes Ice Report API',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: `
|
||||||
|
Community-driven winter road conditions and icy hazards tracker for the Great Lakes region.
|
||||||
|
|
||||||
|
This API allows users to report and retrieve information about icy road conditions,
|
||||||
|
with automatic cleanup after 48 hours and administrative management capabilities.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Report ice conditions with location and description
|
||||||
|
- Retrieve active ice reports (< 48 hours or persistent)
|
||||||
|
- Admin panel for content moderation and management
|
||||||
|
- Profanity filtering for content safety
|
||||||
|
- Geographic data with MapBox integration
|
||||||
|
`,
|
||||||
|
contact: {
|
||||||
|
name: 'Great Lakes Ice Report',
|
||||||
|
url: 'https://github.com/deco/ice'
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
name: 'MIT',
|
||||||
|
url: 'https://opensource.org/licenses/MIT'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
description: 'Development server'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://ice.example.com',
|
||||||
|
description: 'Production server'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: 'Admin authentication using bearer token (admin password)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
Location: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Unique identifier for the location report',
|
||||||
|
example: 123
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Street address, intersection, or landmark description',
|
||||||
|
example: 'Main St & Oak Ave, Grand Rapids, MI'
|
||||||
|
},
|
||||||
|
latitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Geographic latitude coordinate',
|
||||||
|
example: 42.9634,
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Geographic longitude coordinate',
|
||||||
|
example: -85.6681,
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Additional details about the ice conditions',
|
||||||
|
example: 'Multiple vehicles stuck, black ice present',
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
persistent: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether this report persists beyond 48 hours (admin only)',
|
||||||
|
example: false
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'ISO timestamp when the report was created',
|
||||||
|
example: '2025-01-15T10:30:00.000Z'
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'Legacy timestamp field',
|
||||||
|
example: '2025-01-15T10:30:00.000Z'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['address']
|
||||||
|
},
|
||||||
|
LocationInput: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
address: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Street address, intersection, or landmark description',
|
||||||
|
example: 'Main St & Oak Ave, Grand Rapids, MI'
|
||||||
|
},
|
||||||
|
latitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Geographic latitude coordinate (optional, will be geocoded if not provided)',
|
||||||
|
example: 42.9634
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: 'number',
|
||||||
|
format: 'float',
|
||||||
|
description: 'Geographic longitude coordinate (optional, will be geocoded if not provided)',
|
||||||
|
example: -85.6681
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Additional details about the ice conditions',
|
||||||
|
example: 'Black ice spotted, 3 vehicles stuck'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['address']
|
||||||
|
},
|
||||||
|
ProfanityWord: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Unique identifier for the profanity word',
|
||||||
|
example: 1
|
||||||
|
},
|
||||||
|
word: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The profanity word or phrase',
|
||||||
|
example: 'badword'
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['low', 'medium', 'high'],
|
||||||
|
description: 'Severity level of the profanity',
|
||||||
|
example: 'medium'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Category classification of the word',
|
||||||
|
example: 'general'
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
description: 'ISO timestamp when the word was added',
|
||||||
|
example: '2025-01-15T10:30:00.000Z'
|
||||||
|
},
|
||||||
|
created_by: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User who added the word',
|
||||||
|
example: 'admin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['word', 'severity', 'category']
|
||||||
|
},
|
||||||
|
ProfanityWordInput: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
word: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The profanity word or phrase to add',
|
||||||
|
example: 'badword'
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['low', 'medium', 'high'],
|
||||||
|
description: 'Severity level of the profanity',
|
||||||
|
example: 'medium',
|
||||||
|
default: 'medium'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Category classification of the word',
|
||||||
|
example: 'custom',
|
||||||
|
default: 'custom'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['word']
|
||||||
|
},
|
||||||
|
AdminLoginRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
password: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Admin password for authentication',
|
||||||
|
example: 'admin_password_here'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['password']
|
||||||
|
},
|
||||||
|
AdminLoginResponse: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Bearer token for authenticated requests',
|
||||||
|
example: 'admin_password_here'
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Success message',
|
||||||
|
example: 'Login successful'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ApiConfig: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mapboxAccessToken: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'MapBox API token for geocoding (null if not configured)',
|
||||||
|
example: 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJhYmNkZWZnIn0.example',
|
||||||
|
nullable: true
|
||||||
|
},
|
||||||
|
hasMapbox: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether MapBox token is configured',
|
||||||
|
example: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ProfanityTestRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Text to test for profanity',
|
||||||
|
example: 'This is a test message'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['text']
|
||||||
|
},
|
||||||
|
ProfanityTestResponse: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
original: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Original input text',
|
||||||
|
example: 'This is a test message'
|
||||||
|
},
|
||||||
|
analysis: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
hasProfanity: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Whether profanity was detected',
|
||||||
|
example: false
|
||||||
|
},
|
||||||
|
matches: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Array of detected profanity matches',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
word: { type: 'string' },
|
||||||
|
found: { type: 'string' },
|
||||||
|
index: { type: 'integer' },
|
||||||
|
severity: { type: 'string', enum: ['low', 'medium', 'high'] },
|
||||||
|
category: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['none', 'low', 'medium', 'high'],
|
||||||
|
description: 'Overall severity level',
|
||||||
|
example: 'none'
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Number of profanity matches found',
|
||||||
|
example: 0
|
||||||
|
},
|
||||||
|
filtered: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Text with profanity filtered out',
|
||||||
|
example: 'This is a test message'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filtered: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filtered text with profanity removed',
|
||||||
|
example: 'This is a test message'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Error: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
error: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Error message',
|
||||||
|
example: 'Invalid request'
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Detailed error description',
|
||||||
|
example: 'The request contains invalid data'
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Additional error details',
|
||||||
|
additionalProperties: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['error']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 'Public API',
|
||||||
|
description: 'Public endpoints for location reporting and configuration'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Admin - Authentication',
|
||||||
|
description: 'Admin authentication endpoints'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Admin - Locations',
|
||||||
|
description: 'Admin endpoints for location management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Admin - Profanity',
|
||||||
|
description: 'Admin endpoints for profanity filter management'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
apis: ['./src/routes/*.ts', './src/server.ts'], // Paths to files containing OpenAPI definitions
|
||||||
|
};
|
||||||
|
|
||||||
|
export const swaggerSpec = swaggerJsdoc(options);
|
||||||
|
export default swaggerSpec;
|
Loading…
Add table
Add a link
Reference in a new issue