update
This commit is contained in:
parent
e9acfc990c
commit
cada718638
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Troy Goode <troygoode@gmail.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.
|
||||
|
|
@ -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: <matched 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
|
||||
|
|
@ -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;
|
||||
|
||||
}());
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "cors",
|
||||
"description": "Node.js CORS middleware",
|
||||
"version": "2.8.6",
|
||||
"author": "Troy Goode <troygoode@gmail.com> (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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
# object-assign [](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)
|
||||
|
|
@ -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": "<!DOCTYPE html><html><head>...</head><body><div id=\"map\"></div>...</body></html>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
416
server.js
416
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(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenStreetMap Embed</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
#map { width: 100vw; height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([20, 0], 2);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
const defaultIcon = L.Icon.Default;
|
||||
const customIcons = {};
|
||||
|
||||
function createCustomIcon(iconUrl) {
|
||||
if (!iconUrl || iconUrl === 'default') return defaultIcon;
|
||||
if (customIcons[iconUrl]) return customIcons[iconUrl];
|
||||
const icon = L.icon({
|
||||
iconUrl: iconUrl,
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 32],
|
||||
popupAnchor: [0, -32]
|
||||
});
|
||||
customIcons[iconUrl] = icon;
|
||||
return icon;
|
||||
}
|
||||
|
||||
async function loadMarkers() {
|
||||
try {
|
||||
const res = await fetch('/api/markers');
|
||||
const data = await res.json();
|
||||
if (data.markers) {
|
||||
data.markers.forEach(marker => {
|
||||
const icon = createCustomIcon(marker.icon);
|
||||
L.marker([marker.lat, marker.lng], { icon: icon })
|
||||
.addTo(map)
|
||||
.bindPopup(marker.title);
|
||||
});
|
||||
if (data.markers.length > 0) {
|
||||
const last = data.markers[data.markers.length - 1];
|
||||
map.setView([last.lat, last.lng], 13);
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('Error loading markers:', e); }
|
||||
}
|
||||
|
||||
async function loadCircles() {
|
||||
try {
|
||||
const res = await fetch('/api/circles');
|
||||
const data = await res.json();
|
||||
if (data.circles) {
|
||||
data.circles.forEach(circle => {
|
||||
const circleLayer = L.circle([circle.lat, circle.lng], {
|
||||
radius: circle.radius,
|
||||
color: '#3388ff',
|
||||
fillColor: '#3388ff',
|
||||
fillOpacity: 0.3,
|
||||
weight: 2
|
||||
}).addTo(map);
|
||||
if (circle.icon && circle.icon !== 'default') {
|
||||
const icon = createCustomIcon(circle.icon);
|
||||
L.marker([circle.lat, circle.lng], { icon: icon })
|
||||
.addTo(map)
|
||||
.bindPopup(circle.title);
|
||||
} else {
|
||||
circleLayer.bindPopup(circle.title);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) { console.error('Error loading circles:', e); }
|
||||
}
|
||||
|
||||
async function pollAlerts() {
|
||||
try {
|
||||
const res = await fetch('/api/alerts');
|
||||
const data = await res.json();
|
||||
if (data.alerts) {
|
||||
const colors = { danger: '#dc2626', warning: '#f59e0b', info: '#2563eb', success: '#16a34a' };
|
||||
data.alerts.forEach(alert => {
|
||||
const color = colors[alert.type] || colors.danger;
|
||||
L.popup()
|
||||
.setLatLng(map.getCenter())
|
||||
.setContent(\`<div style="background:\${color};color:white;padding:10px;border-radius:5px;">\${alert.message}</div>\`)
|
||||
.openOn(map);
|
||||
});
|
||||
}
|
||||
} catch (e) { console.error('Error polling alerts:', e); }
|
||||
}
|
||||
|
||||
loadMarkers();
|
||||
loadCircles();
|
||||
setInterval(pollAlerts, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>`);
|
||||
});
|
||||
|
||||
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`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue