This commit is contained in:
zhangxunhui 2026-03-29 00:23:50 +08:00
parent e9acfc990c
commit cada718638
14 changed files with 1728 additions and 269 deletions

26
node_modules/.package-lock.json generated vendored
View File

@ -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",

22
node_modules/cors/LICENSE generated vendored Normal file
View File

@ -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.

277
node_modules/cors/README.md generated vendored Normal file
View File

@ -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 theres no error, or an error object to indicate a failure.
- **`corsOptions`**: An object specifying the CORS policy for the current request.
Heres 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

238
node_modules/cors/lib/index.js generated vendored Normal file
View File

@ -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;
}());

42
node_modules/cors/package.json generated vendored Normal file
View File

@ -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"
}
}

90
node_modules/object-assign/index.js generated vendored Normal file
View File

@ -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;
};

21
node_modules/object-assign/license generated vendored Normal file
View File

@ -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.

42
node_modules/object-assign/package.json generated vendored Normal file
View File

@ -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"
}
}

61
node_modules/object-assign/readme.md generated vendored Normal file
View File

@ -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)

676
openapi.json Normal file
View File

@ -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>"
}
}
}
}
}
}
}
}

27
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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
View File

@ -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: '&copy; 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`);
});