diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 49cc59f..bacb094 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -227,6 +227,23 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", @@ -727,6 +744,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/node_modules/cors/LICENSE b/node_modules/cors/LICENSE new file mode 100644 index 0000000..fd10c84 --- /dev/null +++ b/node_modules/cors/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/cors/README.md b/node_modules/cors/README.md new file mode 100644 index 0000000..3d206e5 --- /dev/null +++ b/node_modules/cors/README.md @@ -0,0 +1,277 @@ +# cors + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +CORS is a [Node.js](https://nodejs.org/en/) middleware for [Express](https://expressjs.com/)/[Connect](https://github.com/senchalabs/connect) that sets [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) response headers. These headers tell browsers which origins can read responses from your server. + +> [!IMPORTANT] +> **How CORS Works:** This package sets response headers—it doesn't block requests. CORS is enforced by browsers: they check the headers and decide if JavaScript can read the response. Non-browser clients (curl, Postman, other servers) ignore CORS entirely. See the [MDN CORS guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) for details. + +* [Installation](#installation) +* [Usage](#usage) + * [Simple Usage](#simple-usage-enable-all-cors-requests) + * [Enable CORS for a Single Route](#enable-cors-for-a-single-route) + * [Configuring CORS](#configuring-cors) + * [Configuring CORS w/ Dynamic Origin](#configuring-cors-w-dynamic-origin) + * [Enabling CORS Pre-Flight](#enabling-cors-pre-flight) + * [Customizing CORS Settings Dynamically per Request](#customizing-cors-settings-dynamically-per-request) +* [Configuration Options](#configuration-options) +* [Common Misconceptions](#common-misconceptions) +* [License](#license) +* [Original Author](#original-author) + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/downloading-and-installing-packages-locally): + +```sh +$ npm install cors +``` + +## Usage + +### Simple Usage (Enable *All* CORS Requests) + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +// Adds headers: Access-Control-Allow-Origin: * +app.use(cors()) + +app.get('/products/:id', function (req, res, next) { + res.json({msg: 'Hello'}) +}) + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +### Enable CORS for a Single Route + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +// Adds headers: Access-Control-Allow-Origin: * +app.get('/products/:id', cors(), function (req, res, next) { + res.json({msg: 'Hello'}) +}) + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +### Configuring CORS + +See the [configuration options](#configuration-options) for details. + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +var corsOptions = { + origin: 'http://example.com', + optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 +} + +// Adds headers: Access-Control-Allow-Origin: http://example.com, Vary: Origin +app.get('/products/:id', cors(corsOptions), function (req, res, next) { + res.json({msg: 'Hello'}) +}) + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +### Configuring CORS w/ Dynamic Origin + +This module supports validating the origin dynamically using a function provided +to the `origin` option. This function will be passed a string that is the origin +(or `undefined` if the request has no origin), and a `callback` with the signature +`callback(error, origin)`. + +The `origin` argument to the callback can be any value allowed for the `origin` +option of the middleware, except a function. See the +[configuration options](#configuration-options) section for more information on all +the possible value types. + +This function is designed to allow the dynamic loading of allowed origin(s) from +a backing datasource, like a database. + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +var corsOptions = { + origin: function (origin, callback) { + // db.loadOrigins is an example call to load + // a list of origins from a backing database + db.loadOrigins(function (error, origins) { + callback(error, origins) + }) + } +} + +// Adds headers: Access-Control-Allow-Origin: , Vary: Origin +app.get('/products/:id', cors(corsOptions), function (req, res, next) { + res.json({msg: 'Hello'}) +}) + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +### Enabling CORS Pre-Flight + +Certain CORS requests are considered 'complex' and require an initial +`OPTIONS` request (called the "pre-flight request"). An example of a +'complex' CORS request is one that uses an HTTP verb other than +GET/HEAD/POST (such as DELETE) or that uses custom headers. To enable +pre-flighting, you must add a new OPTIONS handler for the route you want +to support: + +```javascript +var express = require('express') +var cors = require('cors') +var app = express() + +app.options('/products/:id', cors()) // preflight for DELETE +app.del('/products/:id', cors(), function (req, res, next) { + res.json({msg: 'Hello'}) +}) + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +You can also enable pre-flight across-the-board like so: + +```javascript +app.options('*', cors()) // include before other routes +``` + +NOTE: When using this middleware as an application level middleware (for +example, `app.use(cors())`), pre-flight requests are already handled for all +routes. + +### Customizing CORS Settings Dynamically per Request + +For APIs that require different CORS configurations for specific routes or requests, you can dynamically generate CORS options based on the incoming request. The `cors` middleware allows you to achieve this by passing a function instead of static options. This function is called for each incoming request and must use the callback pattern to return the appropriate CORS options. + +The function accepts: +1. **`req`**: + - The incoming request object. + +2. **`callback(error, corsOptions)`**: + - A function used to return the computed CORS options. + - **Arguments**: + - **`error`**: Pass `null` if there’s no error, or an error object to indicate a failure. + - **`corsOptions`**: An object specifying the CORS policy for the current request. + +Here’s an example that handles both public routes and restricted, credential-sensitive routes: + +```javascript +var dynamicCorsOptions = function(req, callback) { + var corsOptions; + if (req.path.startsWith('/auth/connect/')) { + // Access-Control-Allow-Origin: http://mydomain.com, Access-Control-Allow-Credentials: true, Vary: Origin + corsOptions = { + origin: 'http://mydomain.com', + credentials: true + }; + } else { + // Access-Control-Allow-Origin: * + corsOptions = { origin: '*' }; + } + callback(null, corsOptions); +}; + +app.use(cors(dynamicCorsOptions)); + +app.get('/auth/connect/twitter', function (req, res) { + res.send('Hello'); +}); + +app.get('/public', function (req, res) { + res.send('Hello'); +}); + +app.listen(80, function () { + console.log('web server listening on port 80') +}) +``` + +## Configuration Options + +* `origin`: Configures the **Access-Control-Allow-Origin** CORS header. Possible values: + - `Boolean` - set `origin` to `true` to reflect the [request origin](https://datatracker.ietf.org/doc/html/draft-abarth-origin-09), as defined by `req.header('Origin')`, or set it to `false` to disable CORS. + - `String` - set `origin` to a specific origin. For example, if you set it to + - `"http://example.com"` only requests from "http://example.com" will be allowed. + - `"*"` for all domains to be allowed. + - `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com". + - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com". + - `Function` - set `origin` to a function implementing some custom logic. The function takes the request origin as the first parameter and a callback (called as `callback(err, origin)`, where `origin` is a non-function value of the `origin` option) as the second. +* `methods`: Configures the **Access-Control-Allow-Methods** CORS header. Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: `['GET', 'PUT', 'POST']`). +* `allowedHeaders`: Configures the **Access-Control-Allow-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Type,Authorization') or an array (ex: `['Content-Type', 'Authorization']`). If not specified, defaults to reflecting the headers specified in the request's **Access-Control-Request-Headers** header. +* `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed. +* `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted. +* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. Set to an integer to pass the header, otherwise it is omitted. +* `preflightContinue`: Pass the CORS preflight response to the next handler. +* `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`. + +The default configuration is the equivalent of: + +```json +{ + "origin": "*", + "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", + "preflightContinue": false, + "optionsSuccessStatus": 204 +} +``` + +## Common Misconceptions + +### "CORS blocks requests from disallowed origins" + +**No.** Your server receives and processes every request. CORS headers tell the browser whether JavaScript can read the response—not whether the request is allowed. + +### "CORS protects my API from unauthorized access" + +**No.** CORS is not access control. Any HTTP client (curl, Postman, another server) can call your API regardless of CORS settings. Use authentication and authorization to protect your API. + +### "Setting `origin: 'http://example.com'` means only that domain can access my server" + +**No.** It means browsers will only let JavaScript from that origin read responses. The server still responds to all requests. + +## License + +[MIT License](http://www.opensource.org/licenses/mit-license.php) + +## Original Author + +[Troy Goode](https://github.com/TroyGoode) ([troygoode@gmail.com](mailto:troygoode@gmail.com)) + +[coveralls-image]: https://img.shields.io/coveralls/expressjs/cors/master.svg +[coveralls-url]: https://coveralls.io/r/expressjs/cors?branch=master +[downloads-image]: https://img.shields.io/npm/dm/cors.svg +[downloads-url]: https://npmjs.com/package/cors +[github-actions-ci-image]: https://img.shields.io/github/actions/workflow/status/expressjs/cors/ci.yml?branch=master&label=ci +[github-actions-ci-url]: https://github.com/expressjs/cors?query=workflow%3Aci +[npm-image]: https://img.shields.io/npm/v/cors.svg +[npm-url]: https://npmjs.com/package/cors diff --git a/node_modules/cors/lib/index.js b/node_modules/cors/lib/index.js new file mode 100644 index 0000000..ad899ca --- /dev/null +++ b/node_modules/cors/lib/index.js @@ -0,0 +1,238 @@ +(function () { + + 'use strict'; + + var assign = require('object-assign'); + var vary = require('vary'); + + var defaults = { + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', + preflightContinue: false, + optionsSuccessStatus: 204 + }; + + function isString(s) { + return typeof s === 'string' || s instanceof String; + } + + function isOriginAllowed(origin, allowedOrigin) { + if (Array.isArray(allowedOrigin)) { + for (var i = 0; i < allowedOrigin.length; ++i) { + if (isOriginAllowed(origin, allowedOrigin[i])) { + return true; + } + } + return false; + } else if (isString(allowedOrigin)) { + return origin === allowedOrigin; + } else if (allowedOrigin instanceof RegExp) { + return allowedOrigin.test(origin); + } else { + return !!allowedOrigin; + } + } + + function configureOrigin(options, req) { + var requestOrigin = req.headers.origin, + headers = [], + isAllowed; + + if (!options.origin || options.origin === '*') { + // allow any origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: '*' + }]); + } else if (isString(options.origin)) { + // fixed origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: options.origin + }]); + headers.push([{ + key: 'Vary', + value: 'Origin' + }]); + } else { + isAllowed = isOriginAllowed(requestOrigin, options.origin); + // reflect origin + headers.push([{ + key: 'Access-Control-Allow-Origin', + value: isAllowed ? requestOrigin : false + }]); + headers.push([{ + key: 'Vary', + value: 'Origin' + }]); + } + + return headers; + } + + function configureMethods(options) { + var methods = options.methods; + if (methods.join) { + methods = options.methods.join(','); // .methods is an array, so turn it into a string + } + return { + key: 'Access-Control-Allow-Methods', + value: methods + }; + } + + function configureCredentials(options) { + if (options.credentials === true) { + return { + key: 'Access-Control-Allow-Credentials', + value: 'true' + }; + } + return null; + } + + function configureAllowedHeaders(options, req) { + var allowedHeaders = options.allowedHeaders || options.headers; + var headers = []; + + if (!allowedHeaders) { + allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers + headers.push([{ + key: 'Vary', + value: 'Access-Control-Request-Headers' + }]); + } else if (allowedHeaders.join) { + allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string + } + if (allowedHeaders && allowedHeaders.length) { + headers.push([{ + key: 'Access-Control-Allow-Headers', + value: allowedHeaders + }]); + } + + return headers; + } + + function configureExposedHeaders(options) { + var headers = options.exposedHeaders; + if (!headers) { + return null; + } else if (headers.join) { + headers = headers.join(','); // .headers is an array, so turn it into a string + } + if (headers && headers.length) { + return { + key: 'Access-Control-Expose-Headers', + value: headers + }; + } + return null; + } + + function configureMaxAge(options) { + var maxAge = (typeof options.maxAge === 'number' || options.maxAge) && options.maxAge.toString() + if (maxAge && maxAge.length) { + return { + key: 'Access-Control-Max-Age', + value: maxAge + }; + } + return null; + } + + function applyHeaders(headers, res) { + for (var i = 0, n = headers.length; i < n; i++) { + var header = headers[i]; + if (header) { + if (Array.isArray(header)) { + applyHeaders(header, res); + } else if (header.key === 'Vary' && header.value) { + vary(res, header.value); + } else if (header.value) { + res.setHeader(header.key, header.value); + } + } + } + } + + function cors(options, req, res, next) { + var headers = [], + method = req.method && req.method.toUpperCase && req.method.toUpperCase(); + + if (method === 'OPTIONS') { + // preflight + headers.push(configureOrigin(options, req)); + headers.push(configureCredentials(options)) + headers.push(configureMethods(options)) + headers.push(configureAllowedHeaders(options, req)); + headers.push(configureMaxAge(options)) + headers.push(configureExposedHeaders(options)) + applyHeaders(headers, res); + + if (options.preflightContinue) { + next(); + } else { + // Safari (and potentially other browsers) need content-length 0, + // for 204 or they just hang waiting for a body + res.statusCode = options.optionsSuccessStatus; + res.setHeader('Content-Length', '0'); + res.end(); + } + } else { + // actual response + headers.push(configureOrigin(options, req)); + headers.push(configureCredentials(options)) + headers.push(configureExposedHeaders(options)) + applyHeaders(headers, res); + next(); + } + } + + function middlewareWrapper(o) { + // if options are static (either via defaults or custom options passed in), wrap in a function + var optionsCallback = null; + if (typeof o === 'function') { + optionsCallback = o; + } else { + optionsCallback = function (req, cb) { + cb(null, o); + }; + } + + return function corsMiddleware(req, res, next) { + optionsCallback(req, function (err, options) { + if (err) { + next(err); + } else { + var corsOptions = assign({}, defaults, options); + var originCallback = null; + if (corsOptions.origin && typeof corsOptions.origin === 'function') { + originCallback = corsOptions.origin; + } else if (corsOptions.origin) { + originCallback = function (origin, cb) { + cb(null, corsOptions.origin); + }; + } + + if (originCallback) { + originCallback(req.headers.origin, function (err2, origin) { + if (err2 || !origin) { + next(err2); + } else { + corsOptions.origin = origin; + cors(corsOptions, req, res, next); + } + }); + } else { + next(); + } + } + }); + }; + } + + // can pass either an options hash, an options delegate, or nothing + module.exports = middlewareWrapper; + +}()); diff --git a/node_modules/cors/package.json b/node_modules/cors/package.json new file mode 100644 index 0000000..e90bac8 --- /dev/null +++ b/node_modules/cors/package.json @@ -0,0 +1,42 @@ +{ + "name": "cors", + "description": "Node.js CORS middleware", + "version": "2.8.6", + "author": "Troy Goode (https://github.com/troygoode/)", + "license": "MIT", + "keywords": [ + "cors", + "express", + "connect", + "middleware" + ], + "repository": "expressjs/cors", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, + "main": "./lib/index.js", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "devDependencies": { + "after": "0.8.2", + "eslint": "7.30.0", + "express": "4.21.2", + "mocha": "9.2.2", + "nyc": "15.1.0", + "supertest": "6.1.3" + }, + "files": [ + "lib/index.js" + ], + "engines": { + "node": ">= 0.10" + }, + "scripts": { + "test": "npm run lint && npm run test-ci", + "test-ci": "nyc --reporter=lcov --reporter=text mocha --require test/support/env", + "lint": "eslint lib test" + } +} diff --git a/node_modules/object-assign/index.js b/node_modules/object-assign/index.js new file mode 100644 index 0000000..0930cf8 --- /dev/null +++ b/node_modules/object-assign/index.js @@ -0,0 +1,90 @@ +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ + +'use strict'; +/* eslint-disable no-unused-vars */ +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; + +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); +} + +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } +} + +module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; +}; diff --git a/node_modules/object-assign/license b/node_modules/object-assign/license new file mode 100644 index 0000000..654d0bf --- /dev/null +++ b/node_modules/object-assign/license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/node_modules/object-assign/package.json b/node_modules/object-assign/package.json new file mode 100644 index 0000000..503eb1e --- /dev/null +++ b/node_modules/object-assign/package.json @@ -0,0 +1,42 @@ +{ + "name": "object-assign", + "version": "4.1.1", + "description": "ES2015 `Object.assign()` ponyfill", + "license": "MIT", + "repository": "sindresorhus/object-assign", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=0.10.0" + }, + "scripts": { + "test": "xo && ava", + "bench": "matcha bench.js" + }, + "files": [ + "index.js" + ], + "keywords": [ + "object", + "assign", + "extend", + "properties", + "es2015", + "ecmascript", + "harmony", + "ponyfill", + "prollyfill", + "polyfill", + "shim", + "browser" + ], + "devDependencies": { + "ava": "^0.16.0", + "lodash": "^4.16.4", + "matcha": "^0.7.0", + "xo": "^0.16.0" + } +} diff --git a/node_modules/object-assign/readme.md b/node_modules/object-assign/readme.md new file mode 100644 index 0000000..1be09d3 --- /dev/null +++ b/node_modules/object-assign/readme.md @@ -0,0 +1,61 @@ +# object-assign [![Build Status](https://travis-ci.org/sindresorhus/object-assign.svg?branch=master)](https://travis-ci.org/sindresorhus/object-assign) + +> ES2015 [`Object.assign()`](http://www.2ality.com/2014/01/object-assign.html) [ponyfill](https://ponyfill.com) + + +## Use the built-in + +Node.js 4 and up, as well as every evergreen browser (Chrome, Edge, Firefox, Opera, Safari), +support `Object.assign()` :tada:. If you target only those environments, then by all +means, use `Object.assign()` instead of this package. + + +## Install + +``` +$ npm install --save object-assign +``` + + +## Usage + +```js +const objectAssign = require('object-assign'); + +objectAssign({foo: 0}, {bar: 1}); +//=> {foo: 0, bar: 1} + +// multiple sources +objectAssign({foo: 0}, {bar: 1}, {baz: 2}); +//=> {foo: 0, bar: 1, baz: 2} + +// overwrites equal keys +objectAssign({foo: 0}, {foo: 1}, {foo: 2}); +//=> {foo: 2} + +// ignores null and undefined sources +objectAssign({foo: 0}, null, {bar: 1}, undefined); +//=> {foo: 0, bar: 1} +``` + + +## API + +### objectAssign(target, [source, ...]) + +Assigns enumerable own properties of `source` objects to the `target` object and returns the `target` object. Additional `source` objects will overwrite previous ones. + + +## Resources + +- [ES2015 spec - Object.assign](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.assign) + + +## Related + +- [deep-assign](https://github.com/sindresorhus/deep-assign) - Recursive `Object.assign()` + + +## License + +MIT © [Sindre Sorhus](https://sindresorhus.com) diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..c83bfe3 --- /dev/null +++ b/openapi.json @@ -0,0 +1,676 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "OpenStreetMap地图服务", + "version": "1.0.0", + "description": "基于OpenStreetMap的地图可视化服务API,支持在地图上添加标记点、绘制圆形区域、创建和管理告警信息。所有地图元素存储在服务器端,支持通过iframe嵌入到其他网站中。", + "contact": { + "name": "API Support" + }, + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://localhost:3000", + "description": "开发服务器" + } + ], + "tags": [ + { + "name": "Map", + "description": "地图可视化操作接口" + } + ], + "paths": { + "/api/marker": { + "get": { + "summary": "在地图上添加标记点", + "description": "在服务器端存储一个标记点,并根据指定的经纬度坐标在地图上添加该标记,支持自定义图标和弹窗标题。标记数据存储在服务器内存中,可通过 /api/markers 获取所有标记。", + "tags": ["Map"], + "parameters": [ + { + "name": "lat", + "in": "query", + "required": true, + "schema": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "description": "纬度坐标,范围 -90 到 90" + }, + { + "name": "lng", + "in": "query", + "required": true, + "schema": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "description": "经度坐标,范围 -180 到 180" + }, + { + "name": "icon", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "自定义图标URL,默认为OpenStreetMap默认图标" + }, + { + "name": "title", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "标记点的弹窗标题,鼠标悬停时显示" + } + ], + "responses": { + "200": { + "description": "标记点添加成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + }, + "icon": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + }, + "example": { + "success": true, + "type": "marker", + "data": { + "id": "1743168000000", + "lat": 39.9042, + "lng": 116.4074, + "icon": "default", + "title": "北京天安门" + } + } + } + } + }, + "400": { + "description": "参数错误", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "example": { + "error": "Missing required parameters: lat and lng" + } + } + } + } + } + } + }, + "/api/markers": { + "get": { + "summary": "获取所有标记点", + "description": "返回服务器端存储的所有标记点列表", + "tags": ["Map"], + "responses": { + "200": { + "description": "标记列表获取成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "markers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + }, + "icon": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + }, + "example": { + "markers": [ + { + "id": "1743168000000", + "lat": 39.9042, + "lng": 116.4074, + "icon": "default", + "title": "北京天安门" + } + ] + } + } + } + } + } + } + }, + "/api/markers/clear": { + "post": { + "summary": "清除所有标记点", + "description": "清除服务器端存储的所有标记点", + "tags": ["Map"], + "responses": { + "200": { + "description": "标记点清除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "example": { + "success": true + } + } + } + } + } + } + }, + "/api/circle": { + "get": { + "summary": "在地图上绘制圆形区域", + "description": "在服务器端存储一个圆形区域,并根据指定的经纬度坐标和半径在地图上绘制该圆形。圆心位置可选显示标记点。圆形数据存储在服务器内存中,可通过 /api/circles 获取所有圆形。", + "tags": ["Map"], + "parameters": [ + { + "name": "lat", + "in": "query", + "required": true, + "schema": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "description": "圆心纬度坐标,范围 -90 到 90" + }, + { + "name": "lng", + "in": "query", + "required": true, + "schema": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "description": "圆心经度坐标,范围 -180 到 180" + }, + { + "name": "radius", + "in": "query", + "required": true, + "schema": { + "type": "number", + "minimum": 1 + }, + "description": "圆的半径,单位为米,必须大于0" + }, + { + "name": "icon", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "圆心位置的自定义图标URL" + }, + { + "name": "title", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "圆心标记点的弹窗标题" + } + ], + "responses": { + "200": { + "description": "圆形区域绘制成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + }, + "radius": { + "type": "number" + }, + "icon": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + }, + "example": { + "success": true, + "type": "circle", + "data": { + "id": "1743168000001", + "lat": 39.9042, + "lng": 116.4074, + "radius": 1000, + "icon": "default", + "title": "北京市中心区域" + } + } + } + } + }, + "400": { + "description": "参数错误", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "example": { + "error": "Invalid radius: must be a positive number" + } + } + } + } + } + } + }, + "/api/circles": { + "get": { + "summary": "获取所有圆形区域", + "description": "返回服务器端存储的所有圆形区域列表", + "tags": ["Map"], + "responses": { + "200": { + "description": "圆形列表获取成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "circles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lat": { + "type": "number" + }, + "lng": { + "type": "number" + }, + "radius": { + "type": "number" + }, + "icon": { + "type": "string" + }, + "title": { + "type": "string" + } + } + } + } + } + }, + "example": { + "circles": [ + { + "id": "1743168000001", + "lat": 39.9042, + "lng": 116.4074, + "radius": 1000, + "icon": "default", + "title": "北京市中心区域" + } + ] + } + } + } + } + } + } + }, + "/api/circles/clear": { + "post": { + "summary": "清除所有圆形区域", + "description": "清除服务器端存储的所有圆形区域", + "tags": ["Map"], + "responses": { + "200": { + "description": "圆形区域清除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "example": { + "success": true + } + } + } + } + } + } + }, + "/api/alert": { + "post": { + "summary": "创建告警信息", + "description": "在服务器端创建一条告警,告警会以不同颜色的弹窗样式显示在地图界面中,支持danger/warning/info/success四种类型。告警存储在服务器内存中。", + "tags": ["Map"], + "parameters": [ + { + "name": "message", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "告警的文本内容" + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": ["danger", "warning", "info", "success"], + "default": "danger" + }, + "description": "告警类型:danger(红色)、warning(橙色)、info(蓝色)、success(绿色),默认为danger" + } + ], + "responses": { + "200": { + "description": "告警创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "alert": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + } + } + }, + "example": { + "success": true, + "alert": { + "id": "1743168000000", + "message": "前方道路施工", + "type": "warning", + "timestamp": "2026-03-28T12:00:00.000Z" + } + } + } + } + }, + "400": { + "description": "参数错误", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "example": { + "error": "Missing required parameter: message" + } + } + } + } + } + } + }, + "/api/alerts": { + "get": { + "summary": "获取所有告警信息", + "description": "返回服务器端存储的所有告警信息列表", + "tags": ["Map"], + "responses": { + "200": { + "description": "告警列表获取成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "alerts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + } + } + } + } + }, + "example": { + "alerts": [ + { + "id": "1743168000000", + "message": "前方道路施工", + "type": "warning", + "timestamp": "2026-03-28T12:00:00.000Z" + } + ] + } + } + } + } + } + } + }, + "/api/alerts/clear": { + "post": { + "summary": "清除所有告警信息", + "description": "清除服务器端存储的所有告警信息", + "tags": ["Map"], + "responses": { + "200": { + "description": "告警清除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + } + }, + "example": { + "success": true + } + } + } + } + } + } + }, + "/api/embed": { + "get": { + "summary": "获取可嵌入的地图页面", + "description": "返回一个完整的、可嵌入的HTML地图页面。通过iframe方式嵌入到其他网站,页面会自动轮询获取服务器端存储的标记点、圆形区域和告警信息并显示在地图上。", + "tags": ["Map"], + "parameters": [ + { + "name": "lat", + "in": "query", + "required": false, + "schema": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "description": "地图初始化时的中心纬度(此参数已弃用,地图会自动显示所有标记点)" + }, + { + "name": "lng", + "in": "query", + "required": false, + "schema": { + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "description": "地图初始化时的中心经度" + }, + { + "name": "icon", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "初始标记点的自定义图标URL" + }, + { + "name": "title", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "初始标记点的弹窗标题" + } + ], + "responses": { + "200": { + "description": "嵌入用HTML页面", + "content": { + "text/html": { + "schema": { + "type": "string" + }, + "example": "...
..." + } + } + } + } + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 2bee315..a0b93a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "openstreetmap-service", "version": "1.0.0", "dependencies": { + "cors": "^2.8.5", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" @@ -236,6 +237,23 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", @@ -736,6 +754,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index 65834ca..af45916 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "node server.js", "dev": "node server.js" }, - "dependencies": { +"dependencies": { + "cors": "^2.8.5", "express": "^4.18.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1" diff --git a/public/js/map.js b/public/js/map.js index 1497122..fe1b22c 100644 --- a/public/js/map.js +++ b/public/js/map.js @@ -41,10 +41,36 @@ function initMap() { maxZoom: 19 }).addTo(map); + loadServerData(); checkUrlParams(); startAlertPolling(); } +async function loadServerData() { + try { + const [markersRes, circlesRes] = await Promise.all([ + fetch('/api/markers'), + fetch('/api/circles') + ]); + const markersData = await markersRes.json(); + const circlesData = await circlesRes.json(); + + if (markersData.markers) { + markersData.markers.forEach(marker => { + addMarkerToMap(marker.lat, marker.lng, marker.icon, marker.title, false); + }); + } + + if (circlesData.circles) { + circlesData.circles.forEach(circle => { + addCircleToMap(circle.lat, circle.lng, circle.radius, circle.icon, circle.title, false); + }); + } + } catch (error) { + console.error('Error loading server data:', error); + } +} + function startAlertPolling() { pollForAlerts(); alertPollInterval = setInterval(pollForAlerts, 5000); @@ -138,7 +164,7 @@ function checkUrlParams() { const title = params.get('title') || `Marker at ${lat}, ${lng}`; if (!isNaN(lat) && !isNaN(lng)) { - addMarkerToMap(lat, lng, icon, title); + addMarkerToMap(lat, lng, icon, title, false); } } } @@ -163,19 +189,22 @@ function createCustomIcon(iconUrl) { return icon; } -function addMarkerToMap(lat, lng, iconUrl, title) { +function addMarkerToMap(lat, lng, iconUrl, title, addToServer = true) { const icon = createCustomIcon(iconUrl); const marker = L.marker([lat, lng], { icon: icon }) .addTo(map) .bindPopup(title); markers.push(marker); - map.setView([lat, lng], 13); + + if (addToServer || !addToServer) { + map.setView([lat, lng], 13); + } return marker; } -function addCircleToMap(lat, lng, radius, iconUrl, title) { +function addCircleToMap(lat, lng, radius, iconUrl, title, addToServer = true) { const icon = iconUrl && iconUrl !== 'default' ? createCustomIcon(iconUrl) : null; const circle = L.circle([lat, lng], { @@ -193,7 +222,10 @@ function addCircleToMap(lat, lng, radius, iconUrl, title) { } circles.push(circle); - map.setView([lat, lng], 13); + + if (addToServer || !addToServer) { + map.setView([lat, lng], 13); + } return circle; } @@ -219,12 +251,12 @@ async function addMarker() { const data = await response.json(); if (data.success) { - addMarkerToMap(lat, lng, icon, title); + addMarkerToMap(lat, lng, icon, title, false); } else { alert('Error: ' + data.error); } } catch (error) { - addMarkerToMap(lat, lng, icon, title); + alert('Error: ' + error.message); } } @@ -255,12 +287,12 @@ async function addCircle() { const data = await response.json(); if (data.success) { - addCircleToMap(lat, lng, radius, icon, title); + addCircleToMap(lat, lng, radius, icon, title, false); } else { alert('Error: ' + data.error); } } catch (error) { - addCircleToMap(lat, lng, radius, icon, title); + alert('Error: ' + error.message); } } @@ -269,6 +301,12 @@ function clearAll() { circles.forEach(circle => map.removeLayer(circle)); markers = []; circles = []; + + Promise.all([ + fetch('/api/markers/clear', { method: 'POST' }), + fetch('/api/circles/clear', { method: 'POST' }) + ]).catch(error => console.error('Error clearing server data:', error)); + map.setView([20, 0], 2); } diff --git a/server.js b/server.js index 4fdb07f..d101b69 100644 --- a/server.js +++ b/server.js @@ -6,10 +6,16 @@ const swaggerUi = require('swagger-ui-express'); const app = express(); const PORT = process.env.PORT || 3000; +const markers = []; +const circles = []; const alerts = []; app.use(express.static(path.join(__dirname, 'public'))); +app.get('/openapi.json', (req, res) => { + res.sendFile(path.join(__dirname, 'openapi.json')); +}); + const swaggerOptions = { definition: { openapi: '3.0.0', @@ -48,81 +54,6 @@ app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -/** - * @swagger - * /api/marker: - * get: - * summary: Add a marker to the map - * description: Add a marker at the specified geographic coordinates - * tags: [Map] - * parameters: - * - in: query - * name: lat - * required: true - * schema: - * type: number - * minimum: -90 - * maximum: 90 - * description: Latitude coordinate (-90 to 90) - * - in: query - * name: lng - * required: true - * schema: - * type: number - * minimum: -180 - * maximum: 180 - * description: Longitude coordinate (-180 to 180) - * - in: query - * name: icon - * required: false - * schema: - * type: string - * description: URL to a custom icon image - * - in: query - * name: title - * required: false - * schema: - * type: string - * description: Title text for the marker popup - * responses: - * 200: - * description: Marker added successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * type: - * type: string - * example: marker - * data: - * type: object - * properties: - * lat: - * type: number - * example: 39.9042 - * lng: - * type: number - * example: 116.4074 - * icon: - * type: string - * example: default - * title: - * type: string - * example: Beijing - * 400: - * description: Invalid parameters - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - */ app.get('/api/marker', (req, res) => { const { lat, lng, icon, title } = req.query; @@ -147,103 +78,32 @@ app.get('/api/marker', (req, res) => { }); } + const marker = { + id: Date.now().toString(), + lat: latNum, + lng: lngNum, + icon: icon || 'default', + title: title || `Marker at ${latNum}, ${lngNum}` + }; + + markers.push(marker); + res.json({ success: true, type: 'marker', - data: { - lat: latNum, - lng: lngNum, - icon: icon || 'default', - title: title || `Marker at ${latNum}, ${lngNum}` - } + data: marker }); }); -/** - * @swagger - * /api/circle: - * get: - * summary: Add a circle to the map - * description: Add a circle at the specified geographic coordinates with a given radius - * tags: [Map] - * parameters: - * - in: query - * name: lat - * required: true - * schema: - * type: number - * minimum: -90 - * maximum: 90 - * description: Latitude coordinate (-90 to 90) - * - in: query - * name: lng - * required: true - * schema: - * type: number - * minimum: -180 - * maximum: 180 - * description: Longitude coordinate (-180 to 180) - * - in: query - * name: radius - * required: true - * schema: - * type: number - * minimum: 1 - * description: Radius in meters - * - in: query - * name: icon - * required: false - * schema: - * type: string - * description: URL to a custom icon image - * - in: query - * name: title - * required: false - * schema: - * type: string - * description: Title text for the circle popup - * responses: - * 200: - * description: Circle added successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * example: true - * type: - * type: string - * example: circle - * data: - * type: object - * properties: - * lat: - * type: number - * example: 39.9042 - * lng: - * type: number - * example: 116.4074 - * radius: - * type: number - * example: 1000 - * icon: - * type: string - * example: default - * title: - * type: string - * example: Beijing Center - * 400: - * description: Invalid parameters - * content: - * application/json: - * schema: - * type: object - * properties: - * error: - * type: string - */ +app.get('/api/markers', (req, res) => { + res.json({ markers }); +}); + +app.post('/api/markers/clear', (req, res) => { + markers.length = 0; + res.json({ success: true }); +}); + app.get('/api/circle', (req, res) => { const { lat, lng, radius, icon, title } = req.query; @@ -275,65 +135,33 @@ app.get('/api/circle', (req, res) => { }); } + const circle = { + id: Date.now().toString(), + lat: latNum, + lng: lngNum, + radius: radiusNum, + icon: icon || 'default', + title: title || `Circle at ${latNum}, ${lngNum} with radius ${radiusNum}` + }; + + circles.push(circle); + res.json({ success: true, type: 'circle', - data: { - lat: latNum, - lng: lngNum, - radius: radiusNum, - icon: icon || 'default', - title: title || `Circle at ${latNum}, ${lngNum} with radius ${radiusNum}` - } + data: circle }); }); -/** - * @swagger - * /api/alert: - * post: - * summary: Create an alert message - * description: Creates an alert that will be displayed on the map interface. The map page polls for new alerts. - * tags: [Map] - * parameters: - * - in: query - * name: message - * required: true - * schema: - * type: string - * description: The alert message text to display - * - in: query - * name: type - * required: false - * schema: - * type: string - * enum: [danger, warning, info, success] - * default: danger - * description: Alert type - danger (red), warning (orange), info (blue), success (green) - * responses: - * 200: - * description: Alert created successfully - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * alert: - * type: object - * properties: - * id: - * type: string - * message: - * type: string - * type: - * type: string - * timestamp: - * type: string - * 400: - * description: Missing required parameter - */ +app.get('/api/circles', (req, res) => { + res.json({ circles }); +}); + +app.post('/api/circles/clear', (req, res) => { + circles.length = 0; + res.json({ success: true }); +}); + app.post('/api/alert', (req, res) => { const { message, type } = req.query || req.body; @@ -355,56 +183,126 @@ app.post('/api/alert', (req, res) => { res.json({ success: true, alert }); }); -/** - * @swagger - * /api/alerts: - * get: - * summary: Get all active alerts - * description: Returns all active alerts that have not been dismissed - * tags: [Map] - * responses: - * 200: - * description: List of alerts - * content: - * application/json: - * schema: - * type: object - * properties: - * alerts: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * message: - * type: string - * type: - * type: string - * timestamp: - * type: string - */ app.get('/api/alerts', (req, res) => { res.json({ alerts }); }); -/** - * @swagger - * /api/alerts/clear: - * post: - * summary: Clear all alerts - * description: Removes all active alerts from the map - * tags: [Map] - * responses: - * 200: - * description: Alerts cleared - */ app.post('/api/alerts/clear', (req, res) => { alerts.length = 0; res.json({ success: true }); }); +app.get('/api/embed', (req, res) => { + res.send(` + + + + + OpenStreetMap Embed + + + + +
+ + + +`); +}); + app.listen(PORT, () => { console.log(`OpenStreetMap Service running at http://localhost:${PORT}`); console.log(`Swagger docs available at http://localhost:${PORT}/api-docs`); + console.log(`Embed API available at http://localhost:${PORT}/api/embed`); });