From e98dbcb0f4231bc967dfe2c02ed52ec938eef140 Mon Sep 17 00:00:00 2001 From: ZHENG XIAOYI Date: Fri, 6 Feb 2026 14:29:29 +0800 Subject: [PATCH] Initial commit --- .gitignore | 74 +++ README.md | 106 ++++ middleware/auth.js | 46 ++ package.json | 24 + pnpm-lock.yaml | 1149 +++++++++++++++++++++++++++++++++++++++++++ routes/auth.js | 139 ++++++ routes/companies.js | 407 +++++++++++++++ routes/serials.js | 407 +++++++++++++++ scripts/init-db.js | 97 ++++ server.js | 75 +++ utils/database.js | 50 ++ 11 files changed, 2574 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 middleware/auth.js create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 routes/auth.js create mode 100644 routes/companies.js create mode 100644 routes/serials.js create mode 100644 scripts/init-db.js create mode 100644 server.js create mode 100644 utils/database.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b10dac8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Build files +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e90138 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# 授权管理系统 - 后端服务 + +浙江贝凡企业授权管理系统的后端服务,基于 Node.js + Express + SQLite。 + +## 技术栈 + +- **Node.js**: 运行时环境 +- **Express**: Web 框架 +- **SQLite**: 数据库 +- **JWT**: 身份认证 +- **bcryptjs**: 密码加密 + +## 项目结构 + +``` +backend/ +├── routes/ # API 路由 +│ ├── auth.js # 认证路由 +│ ├── serials.js # 序列号路由 +│ └── companies.js # 企业路由 +├── middleware/ # 中间件 +│ └── auth.js # 认证中间件 +├── scripts/ # 脚本 +│ └── init-db.js # 数据库初始化 +├── utils/ # 工具函数 +│ └── database.js # 数据库连接 +├── data/ # 数据文件 +│ └── database.sqlite +├── server.js # 服务器入口 +├── .env # 环境变量 +└── package.json # 项目配置 +``` + +## 安装 + +```bash +pnpm install +``` + +## 开发 + +启动开发服务器(支持热重载): + +```bash +pnpm dev +``` + +服务器将在 http://localhost:3000 运行 + +## 生产 + +启动生产服务器: + +```bash +pnpm start +``` + +## 数据库初始化 + +初始化数据库和默认管理员账户: + +```bash +pnpm init-db +``` + +## 环境变量 + +创建 `.env` 文件: + +```env +PORT=3000 +JWT_SECRET=your-secret-key-here +``` + +## 默认账户 + +初始化后默认创建的管理员账户: +- 用户名: admin +- 密码: Beifan@2026 + +## API 接口 + +### 认证接口 + +- `POST /api/auth/login` - 用户登录 +- `POST /api/auth/logout` - 用户登出 +- `POST /api/auth/change-password` - 修改密码 + +### 序列号接口 + +- `POST /api/serials/generate` - 生成序列号 +- `GET /api/serials/:serialNumber/query` - 查询序列号 +- `POST /api/serials/:serialNumber/revoke` - 吊销序列号 +- `GET /api/serials/` - 获取序列号列表 + +### 企业接口 + +- `GET /api/companies/` - 获取企业列表 +- `GET /api/companies/:companyName` - 获取企业详情 +- `POST /api/companies/:companyName/revoke` - 吊销企业 +- `DELETE /api/companies/:companyName` - 删除企业 +- `GET /api/companies/stats/overview` - 获取统计数据 + +## License + +MIT diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..7e577ff --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,46 @@ +const jwt = require('jsonwebtoken'); +const db = require('../utils/database'); + +// 验证JWT令牌 +const authenticateToken = async (req, res, next) => { + // 从请求头获取令牌 + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: '访问令牌缺失' }); + } + + try { + // 验证令牌 + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // 获取用户信息 + const user = await db.get('SELECT id, username, name, role FROM users WHERE id = ?', [decoded.userId]); + + if (!user) { + return res.status(401).json({ error: '用户不存在' }); + } + + req.user = user; + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ error: '令牌已过期' }); + } + return res.status(403).json({ error: '无效的令牌' }); + } +}; + +// 验证管理员权限 +const requireAdmin = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ error: '需要管理员权限' }); + } + next(); +}; + +module.exports = { + authenticateToken, + requireAdmin +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..60a87b9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "trace-backend", + "version": "1.0.0", + "description": "浙江贝凡企业授权管理系统 - 后端服务", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "init-db": "node scripts/init-db.js" + }, + "dependencies": { + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.6.2", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "author": "", + "license": "MIT" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..901e6c5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1149 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^17.2.3 + version: 17.2.4 + express: + specifier: ^5.2.1 + version: 5.2.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.3 + devDependencies: + nodemon: + specifier: ^3.0.1 + version: 3.1.11 + +packages: + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bcryptjs@3.0.3: + resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} + hasBin: true + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.2.4: + resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + nodemon@3.1.11: + resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + bcryptjs@3.0.3: {} + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + concat-map@0.0.1: {} + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.2.4: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + expand-template@2.0.3: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + + has-flag@3.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ipaddr.js@1.9.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + ms@2.1.3: {} + + napi-build-utils@2.0.0: {} + + negotiator@1.0.0: {} + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + nodemon@3.1.11: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.4 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-to-regexp@8.3.0: {} + + picomatch@2.3.1: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pstree.remy@1.1.8: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.14.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.4 + + statuses@2.0.2: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + undefsafe@2.0.5: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + wrappy@1.0.2: {} diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..5642e77 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,139 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const db = require('../utils/database'); +const { authenticateToken } = require('../middleware/auth'); + +const router = express.Router(); + +// 用户登录 +router.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: '用户名和密码不能为空' }); + } + + // 查询用户 + const user = await db.get('SELECT * FROM users WHERE username = ?', [username]); + + if (!user) { + return res.status(401).json({ error: '用户名或密码错误' }); + } + + // 验证密码 + const isValidPassword = await bcrypt.compare(password, user.password); + + if (!isValidPassword) { + return res.status(401).json({ error: '用户名或密码错误' }); + } + + // 生成JWT令牌 + const token = jwt.sign( + { userId: user.id, username: user.username, role: user.role }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + + // 返回用户信息和令牌 + res.json({ + accessToken: token, + user: { + id: user.id, + username: user.username, + name: user.name, + email: user.email, + role: user.role + } + }); + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取用户信息 +router.get('/profile', authenticateToken, async (req, res) => { + try { + const user = await db.get('SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', [req.user.id]); + + if (!user) { + return res.status(404).json({ error: '用户不存在' }); + } + + res.json(user); + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 修改密码 +router.post('/change-password', authenticateToken, async (req, res) => { + try { + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({ error: '当前密码和新密码不能为空' }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ error: '新密码长度至少为6位' }); + } + + // 查询用户 + const user = await db.get('SELECT password FROM users WHERE id = ?', [req.user.id]); + + if (!user) { + return res.status(404).json({ error: '用户不存在' }); + } + + // 验证当前密码 + const isValidPassword = await bcrypt.compare(currentPassword, user.password); + + if (!isValidPassword) { + return res.status(401).json({ error: '当前密码错误' }); + } + + // 哈希新密码 + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // 更新密码 + await db.run('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hashedPassword, req.user.id]); + + res.json({ message: '密码修改成功' }); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 更新用户资料 +router.put('/profile', authenticateToken, async (req, res) => { + try { + const { name, email } = req.body; + + // 验证输入 + if (!name) { + return res.status(400).json({ error: '姓名不能为空' }); + } + + if (email && !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ error: '邮箱格式不正确' }); + } + + // 更新用户资料 + await db.run('UPDATE users SET name = ?, email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [name, email, req.user.id]); + + // 获取更新后的用户资料 + const updatedUser = await db.get('SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', [req.user.id]); + + res.json(updatedUser); + } catch (error) { + console.error('更新用户资料错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/companies.js b/routes/companies.js new file mode 100644 index 0000000..939cfea --- /dev/null +++ b/routes/companies.js @@ -0,0 +1,407 @@ +const express = require('express'); +const db = require('../utils/database'); +const { authenticateToken, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); + +// 获取企业列表 +router.get('/', authenticateToken, requireAdmin, async (req, res) => { + try { + const { page = 1, limit = 20, search = '' } = req.query; + const offset = (page - 1) * limit; + + let query = ` + SELECT c.company_name, c.created_at as first_created, c.updated_at as last_created, c.is_active, + (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name) as serial_count, + (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name AND s.is_active = 1) as active_count + FROM companies c + `; + let countQuery = 'SELECT COUNT(*) as total FROM companies'; + let params = []; + + if (search) { + query += ' WHERE c.company_name LIKE ?'; + countQuery += ' WHERE company_name LIKE ?'; + params.push(`%${search}%`); + } + + query += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const [companies, countResult] = await Promise.all([ + db.all(query, params), + db.get(countQuery, params.slice(0, -2)) + ]); + + const total = countResult ? countResult.total : 0; + const totalPages = Math.ceil(total / limit); + + res.json({ + message: '获取企业列表成功', + data: companies.map(company => ({ + companyName: company.company_name, + firstCreated: company.first_created, + lastCreated: company.last_created, + serialCount: company.serial_count, + activeCount: company.active_count, + status: company.is_active ? 'active' : 'disabled' + })), + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + }); + } catch (error) { + console.error('获取企业列表错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取企业详情 +router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName } = req.params; + const decodedCompanyName = decodeURIComponent(companyName); + const { page = 1, limit = 20 } = req.query; + const offset = (page - 1) * limit; + + // 获取企业基本信息 + const companyInfo = await db.get('SELECT * FROM companies WHERE company_name = ?', [decodedCompanyName]); + + if (!companyInfo) { + return res.status(404).json({ error: '企业不存在' }); + } + + // 获取序列号统计信息 + const serialStats = await db.get(` + SELECT COUNT(*) as serial_count, + SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_count, + SUM(CASE WHEN is_active = 0 THEN 1 ELSE 0 END) as disabled_count, + SUM(CASE WHEN valid_until IS NOT NULL AND valid_until <= datetime('now') THEN 1 ELSE 0 END) as expired_count + FROM serials + WHERE company_name = ? + `, [decodedCompanyName]); + + // 获取企业的序列号列表 + const serials = await db.all(` + SELECT s.*, u.name as created_by_name + FROM serials s + LEFT JOIN users u ON s.created_by = u.id + WHERE s.company_name = ? + ORDER BY s.created_at DESC + LIMIT ? OFFSET ? + `, [decodedCompanyName, parseInt(limit), parseInt(offset)]); + + // 获取统计数据 + const stats = await db.all(` + SELECT strftime('%Y-%m', created_at) as month, + COUNT(*) as count + FROM serials + WHERE company_name = ? + GROUP BY strftime('%Y-%m', created_at) + ORDER BY month DESC + LIMIT 12 + `, [decodedCompanyName]); + + res.json({ + message: '获取企业详情成功', + data: { + companyName: decodedCompanyName, + serialCount: serialStats?.serial_count || 0, + activeCount: serialStats?.active_count || 0, + disabledCount: serialStats?.disabled_count || 0, + expiredCount: serialStats?.expired_count || 0, + firstCreated: companyInfo.created_at, + lastCreated: companyInfo.updated_at, + status: companyInfo.is_active ? 'active' : 'disabled', + serials: serials.map(s => ({ + serialNumber: s.serial_number, + validUntil: s.valid_until, + isActive: s.is_active, + createdAt: s.created_at, + createdBy: s.created_by_name + })), + monthlyStats: stats.map(stat => ({ + month: stat.month, + count: stat.count + })) + } + }); + } catch (error) { + console.error('获取企业详情错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 更新企业信息 +router.patch('/:companyName', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName } = req.params; + const decodedCompanyName = decodeURIComponent(companyName); + const { newCompanyName } = req.body; + + if (!newCompanyName || newCompanyName.trim() === '') { + return res.status(400).json({ error: '新企业名称不能为空' }); + } + + // 检查企业是否存在 + const existingCompany = db.get( + 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', + [decodedCompanyName] + ); + + if (!existingCompany || existingCompany.count === 0) { + return res.status(404).json({ error: '企业不存在' }); + } + + // 检查新企业名称是否已存在 + const duplicateCompany = db.get( + 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', + [newCompanyName] + ); + + if (duplicateCompany && duplicateCompany.count > 0) { + return res.status(400).json({ error: '企业名称已存在' }); + } + + // 更新企业名称 + db.run( + 'UPDATE serials SET company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?', + [newCompanyName, decodedCompanyName] + ); + + res.json({ + message: '企业名称更新成功', + data: { + oldCompanyName: decodedCompanyName, + newCompanyName + } + }); + } catch (error) { + console.error('更新企业信息错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 删除企业(物理删除,完全删除企业和所有序列号) +router.delete('/:companyName', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName } = req.params; + console.log('原始参数:', companyName); + const decodedCompanyName = decodeURIComponent(companyName); + console.log('解码后参数:', decodedCompanyName); + + // 检查企业是否存在 + const existingCompany = await db.get( + 'SELECT * FROM companies WHERE company_name = ?', + [decodedCompanyName] + ); + + if (!existingCompany) { + return res.status(404).json({ error: '企业不存在' }); + } + + // 开始事务 + db.run('BEGIN TRANSACTION'); + + try { + // 删除该企业的所有序列号 + const serialDeleteResult = db.run( + 'DELETE FROM serials WHERE company_name = ?', + [decodedCompanyName] + ); + console.log('删除序列号结果:', serialDeleteResult); + + // 删除企业记录 + const companyDeleteResult = db.run( + 'DELETE FROM companies WHERE company_name = ?', + [decodedCompanyName] + ); + console.log('删除企业结果:', companyDeleteResult); + + if (companyDeleteResult.changes === 0) { + db.run('ROLLBACK'); + return res.status(404).json({ error: '企业不存在' }); + } + + db.run('COMMIT'); + + res.json({ + message: '企业已完全删除,所有相关序列号已删除', + data: { + companyName: decodedCompanyName, + deletedSerialCount: serialDeleteResult.changes, + deletedCompanyCount: companyDeleteResult.changes + } + }); + } catch (error) { + console.error('删除企业过程中错误:', error); + db.run('ROLLBACK'); + throw error; + } + } catch (error) { + console.error('删除企业错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取企业统计数据 +router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res) => { + try { + // 获取总企业数 + const companyCount = await db.get('SELECT COUNT(*) as count FROM companies'); + + // 获取总序列号数 + const serialCount = await db.get('SELECT COUNT(*) as count FROM serials'); + + // 获取活跃序列号数 + const activeCount = await db.get(` + SELECT COUNT(*) as count FROM serials + WHERE is_active = 1 AND (valid_until IS NULL OR valid_until > datetime('now')) + `); + + // 按月份统计 - 使用正确的日期格式 + const monthlyStats = await db.all(` + SELECT strftime('%Y-%m', created_at) as month, + COUNT(DISTINCT company_name) as company_count, + COUNT(*) as serial_count + FROM serials + WHERE created_at >= strftime('%Y-%m-%d', datetime('now', '-12 months')) + GROUP BY strftime('%Y-%m', created_at) + ORDER BY month ASC + `); + + // 获取最新添加的企业 + const recentCompanies = await db.all(` + SELECT c.company_name, c.created_at as last_created, c.is_active + FROM companies c + ORDER BY c.updated_at DESC + LIMIT 10 + `); + + // 获取最近生成的序列号 + const recentSerials = await db.all(` + SELECT s.serial_number, s.company_name, s.is_active, s.created_at + FROM serials s + ORDER BY s.created_at DESC + LIMIT 10 + `); + + // 如果没有数据,生成过去12个月的空数据 + if (monthlyStats.length === 0) { + const now = new Date(); + for (let i = 11; i >=0; i--) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const month = date.toISOString().substr(0, 7); + monthlyStats.push({ + month, + company_count: 0, + serial_count: 0 + }); + } + } + + res.json({ + message: '获取统计数据成功', + data: { + overview: { + totalCompanies: companyCount.count || 0, + totalSerials: serialCount.count || 0, + activeSerials: activeCount.count || 0, + inactiveSerials: (serialCount.count || 0) - (activeCount.count || 0) + }, + monthlyStats: monthlyStats.map(stat => ({ + month: stat.month, + company_count: stat.company_count, + serial_count: stat.serial_count + })), + recentCompanies: recentCompanies.map(c => ({ + companyName: c.company_name, + lastCreated: c.last_created, + status: c.is_active ? 'active' : 'disabled' + })), + recentSerials: recentSerials.map(s => ({ + serialNumber: s.serial_number, + companyName: s.company_name, + isActive: s.is_active, + createdAt: s.created_at + })) + } + }); + } catch (error) { + console.error('获取统计数据错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 吊销单个序列号 +router.delete('/:companyName/serials/:serialNumber', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName, serialNumber } = req.params; + + // 检查序列号是否存在且属于该企业 + const serial = await db.get( + 'SELECT * FROM serials WHERE serial_number = ? AND company_name = ?', + [serialNumber.toUpperCase(), companyName] + ); + + if (!serial) { + return res.status(404).json({ error: '序列号不存在或不属于该企业' }); + } + + // 物理删除序列号 + await db.run( + 'DELETE FROM serials WHERE serial_number = ? AND company_name = ?', + [serialNumber.toUpperCase(), companyName] + ); + + res.json({ + message: '序列号已成功吊删除', + data: { + serialNumber: serial.serial_number, + companyName + } + }); + } catch (error) { + console.error('吊销序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 吊销企业 +router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName } = req.params; + const decodedCompanyName = decodeURIComponent(companyName); + + // 检查企业是否存在 + const existingCompany = await db.get( + 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', + [decodedCompanyName] + ); + + if (!existingCompany || existingCompany.count === 0) { + return res.status(404).json({ error: '企业不存在' }); + } + + // 吊销该企业的所有序列号(将 is_active 设为 0) + await db.run( + 'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?', + [decodedCompanyName] + ); + + res.json({ + message: '企业已吊销,所有序列号已失效', + data: { + companyName: decodedCompanyName + } + }); + } catch (error) { + console.error('吊销企业错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/serials.js b/routes/serials.js new file mode 100644 index 0000000..b268d74 --- /dev/null +++ b/routes/serials.js @@ -0,0 +1,407 @@ +const express = require('express'); +const QRCode = require('qrcode'); +const db = require('../utils/database'); +const { authenticateToken, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); + +// 生成序列号 +router.post('/generate', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName, quantity = 1, validDays = 365 } = req.body; + + if (!companyName) { + return res.status(400).json({ error: '企业名称不能为空' }); + } + + if (quantity < 1 || quantity > 100) { + return res.status(400).json({ error: '生成数量必须在1-100之间' }); + } + + // 计算有效期 + const validUntil = new Date(); + validUntil.setDate(validUntil.getDate() + validDays); + + // 确保企业存在 + const existingCompany = await db.get('SELECT * FROM companies WHERE company_name = ?', [companyName]); + if (!existingCompany) { + await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]); + } + + // 生成序列号 + const serials = []; + const prefix = 'BF'; + const datePart = new Date().getFullYear().toString().substr(2); + + // 批量插入序列号 + const insertPromises = []; + + for (let i = 0; i < quantity; i++) { + // 使用随机数生成序列号,避免重复 + const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0'); + const serialNumber = `${prefix}${datePart}${randomPart}`; + + insertPromises.push( + db.run( + 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', + [serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), req.user.id] + ) + ); + + serials.push({ + serialNumber, + companyName, + validUntil: validUntil.toISOString(), + createdAt: new Date().toISOString() + }); + } + + await Promise.all(insertPromises); + + res.json({ + message: `成功生成${quantity}个序列号`, + serials + }); + } catch (error) { + console.error('生成序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 生成二维码 +router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => { + try { + const { serialNumber } = req.params; + let { baseUrl } = req.body; + + if (!serialNumber) { + return res.status(400).json({ error: '序列号不能为空' }); + } + + // 验证序列号是否存在 + const serial = await db.get( + 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', + [serialNumber.toUpperCase()] + ); + + if (!serial) { + return res.status(404).json({ error: '序列号不存在' }); + } + + if (!serial.is_active) { + return res.status(400).json({ error: '序列号已被禁用' }); + } + + // 检查是否过期 + if (serial.valid_until && new Date(serial.valid_until) < new Date()) { + return res.status(400).json({ error: '序列号已过期' }); + } + + // 生成查询URL + if (!baseUrl) { + baseUrl = `${req.protocol}://${req.get('host')}/query.html`; + } + + const queryUrl = baseUrl.includes('?') + ? `${baseUrl}&serial=${serial.serial_number}` + : `${baseUrl}?serial=${serial.serial_number}`; + + // 生成二维码 + const qrCodeData = await QRCode.toDataURL(queryUrl, { + width: 200, + color: { + dark: '#165DFF', + light: '#ffffff' + } + }); + + res.json({ + message: '二维码生成成功', + qrCodeData, + queryUrl, + serialNumber: serial.serial_number, + companyName: serial.company_name, + validUntil: serial.valid_until + }); + } catch (error) { + console.error('生成二维码错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 查询序列号 +router.get('/:serialNumber/query', async (req, res) => { + try { + const { serialNumber } = req.params; + + if (!serialNumber) { + return res.status(400).json({ error: '序列号不能为空' }); + } + + // 查询序列号 + const serial = await db.get( + 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', + [serialNumber.toUpperCase()] + ); + + if (!serial) { + return res.status(404).json({ error: '序列号不存在' }); + } + + // 检查是否过期 + if (serial.valid_until && new Date(serial.valid_until) < new Date()) { + return res.status(400).json({ error: '序列号已过期' }); + } + + res.json({ + message: '查询成功', + serial: { + serialNumber: serial.serial_number, + companyName: serial.company_name, + validUntil: serial.valid_until, + status: serial.is_active ? 'active' : 'disabled', + isActive: serial.is_active, + createdAt: serial.created_at, + createdBy: serial.created_by_name + } + }); + } catch (error) { + console.error('查询序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 获取序列号列表 +router.get('/', authenticateToken, async (req, res) => { + try { + const { page = 1, limit = 20, search = '' } = req.query; + const offset = (page - 1) * limit; + + let query = ` + SELECT s.*, u.name as created_by_name + FROM serials s + LEFT JOIN users u ON s.created_by = u.id + `; + let countQuery = 'SELECT COUNT(*) as total FROM serials s'; + let params = []; + + if (search) { + query += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?'; + countQuery += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?'; + const searchParam = `%${search}%`; + params.push(searchParam, searchParam); + } + + query += ' ORDER BY s.created_at DESC LIMIT ? OFFSET ?'; + params.push(parseInt(limit), parseInt(offset)); + + const [serials, countResult] = await Promise.all([ + db.all(query, params), + db.get(countQuery, params.slice(0, -2)) + ]); + + const total = countResult ? countResult.total : 0; + const totalPages = Math.ceil(total / limit); + + res.json({ + message: '获取序列号列表成功', + data: serials.map(s => ({ + serialNumber: s.serial_number, + companyName: s.company_name, + validUntil: s.valid_until, + isActive: s.is_active, + createdAt: s.created_at, + createdBy: s.created_by_name + })), + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + }); + } catch (error) { + console.error('获取序列号列表错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 更新序列号 +router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res) => { + try { + const { serialNumber } = req.params; + const { companyName, validUntil, isActive } = req.body; + + if (!serialNumber) { + return res.status(400).json({ error: '序列号不能为空' }); + } + + // 检查序列号是否存在 + const existingSerial = await db.get('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]); + + if (!existingSerial) { + return res.status(404).json({ error: '序列号不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const params = []; + + if (companyName !== undefined) { + updateFields.push('company_name = ?'); + params.push(companyName); + } + + if (validUntil !== undefined) { + updateFields.push('valid_until = ?'); + params.push(validUntil); + } + + if (isActive !== undefined) { + updateFields.push('is_active = ?'); + params.push(isActive ? 1 : 0); + } + + if (updateFields.length === 0) { + return res.status(400).json({ error: '没有提供更新字段' }); + } + + updateFields.push('updated_at = CURRENT_TIMESTAMP'); + params.push(serialNumber.toUpperCase()); + + await db.run( + `UPDATE serials SET ${updateFields.join(', ')} WHERE serial_number = ?`, + params + ); + + // 获取更新后的序列号信息 + const updatedSerial = await db.get( + 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', + [serialNumber.toUpperCase()] + ); + + res.json({ + message: '序列号更新成功', + serial: { + serialNumber: updatedSerial.serial_number, + companyName: updatedSerial.company_name, + validUntil: updatedSerial.valid_until, + isActive: updatedSerial.is_active, + createdAt: updatedSerial.created_at, + updatedAt: updatedSerial.updated_at, + createdBy: updatedSerial.created_by_name + } + }); + } catch (error) { + console.error('更新序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 吊销序列号 +router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req, res) => { + try { + const { serialNumber } = req.params; + + if (!serialNumber) { + return res.status(400).json({ error: '序列号不能为空' }); + } + + // 检查序列号是否存在 + const existingSerial = await db.get( + 'SELECT * FROM serials WHERE serial_number = ?', + [serialNumber.toUpperCase()] + ); + + if (!existingSerial) { + return res.status(404).json({ error: '序列号不存在' }); + } + + // 如果已经吊销,返回提示 + if (!existingSerial.is_active) { + return res.status(400).json({ error: '序列号已被吊销' }); + } + + // 吊销序列号(将 is_active 设为 0) + await db.run( + 'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?', + [serialNumber.toUpperCase()] + ); + + res.json({ + message: '序列号已吊销', + data: { + serialNumber: serialNumber.toUpperCase() + } + }); + } catch (error) { + console.error('吊销序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +// 自定义前缀生成序列号(管理员权限) +router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req, res) => { + try { + const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body; + + if (!companyName) { + return res.status(400).json({ error: '企业名称不能为空' }); + } + + if (!serialPrefix || serialPrefix.length > 10) { + return res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' }); + } + + if (quantity < 1 || quantity > 100) { + return res.status(400).json({ error: '生成数量必须在1-100之间' }); + } + + // 计算有效期 + const validUntil = new Date(); + validUntil.setDate(validUntil.getDate() + validDays); + + // 生成序列号 + const serials = []; + const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, ''); + + if (!prefix) { + return res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' }); + } + + // 批量插入序列号 + const insertPromises = []; + + for (let i = 0; i < quantity; i++) { + // 使用随机数生成序列号,避免重复 + const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0'); + const serialNumber = `${prefix}${randomPart}`; + + insertPromises.push( + db.run( + 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', + [serialNumber, companyName, validUntil.toISOString(), req.user.id] + ) + ); + + serials.push({ + serialNumber, + companyName, + validUntil: validUntil.toISOString(), + createdAt: new Date().toISOString() + }); + } + + await Promise.all(insertPromises); + + res.json({ + message: `成功生成${quantity}个序列号`, + serials + }); + } catch (error) { + console.error('生成序列号错误:', error); + res.status(500).json({ error: '服务器内部错误' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/scripts/init-db.js b/scripts/init-db.js new file mode 100644 index 0000000..c6770da --- /dev/null +++ b/scripts/init-db.js @@ -0,0 +1,97 @@ +const Database = require('better-sqlite3'); +const bcrypt = require('bcryptjs'); +const path = require('path'); + +// 创建数据库连接 +const dbPath = path.join(__dirname, '../data/database.sqlite'); +const db = new Database(dbPath, { verbose: console.log }); + +// 创建表 +const createTables = () => { + // 用户表 + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + name TEXT NOT NULL, + email TEXT, + role TEXT DEFAULT 'user', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 企业表 + db.exec(` + CREATE TABLE IF NOT EXISTS companies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_name TEXT UNIQUE NOT NULL, + is_active BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 序列号表 + db.exec(` + CREATE TABLE IF NOT EXISTS serials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + serial_number TEXT UNIQUE NOT NULL, + company_name TEXT NOT NULL, + valid_until DATETIME, + is_active BOOLEAN DEFAULT 1, + created_by INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (created_by) REFERENCES users (id), + FOREIGN KEY (company_name) REFERENCES companies (company_name) + ) + `); + + // 创建索引 + db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_companies ON companies (company_name)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_serial_number ON serials (serial_number)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_serials ON serials (company_name)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_created_by ON serials (created_by)'); + + console.log('数据库表创建完成'); +}; + +// 创建默认管理员用户 +const createDefaultUser = async () => { + const username = 'admin'; + const password = 'Beifan@2026'; + const name = '系统管理员'; + + // 检查用户是否已存在 + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); + + if (!user) { + // 哈希密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户 + db.prepare( + 'INSERT INTO users (username, password, name, email, role) VALUES (?, ?, ?, ?, ?)' + ).run(username, hashedPassword, name, 'admin@example.com', 'admin'); + + console.log('默认管理员用户创建完成:'); + console.log('用户名:', username); + console.log('密码:', password); + } else { + console.log('默认管理员用户已存在'); + } +}; + +// 初始化数据库 +const initDatabase = async () => { + createTables(); + await createDefaultUser(); + + // 关闭数据库连接 + db.close(); + console.log('数据库连接已关闭'); +}; + +initDatabase(); \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..a269d9f --- /dev/null +++ b/server.js @@ -0,0 +1,75 @@ +require('dotenv').config({ path: __dirname + '/.env' }); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); + +// 导入路由 +const authRoutes = require('./routes/auth'); +const serialRoutes = require('./routes/serials'); +const companyRoutes = require('./routes/companies'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 中间件 +app.use(cors()); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); +// 设置响应编码 +app.use((req, res, next) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + next(); +}); + +// API路由(必须在静态文件服务之前) +app.use('/api/auth', authRoutes); +app.use('/api/serials', serialRoutes); +app.use('/api/companies', companyRoutes); + +// 健康检查 +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', message: '服务器运行正常' }); +}); + +// 静态文件服务 +const frontendPath = path.join(__dirname, '..', 'frontend'); +const distPath = path.join(frontendPath, 'dist'); +const publicPath = path.join(frontendPath, 'public'); + +if (process.env.NODE_ENV === 'production') { + app.use(express.static(distPath)); +} else { + app.use(express.static(publicPath)); +} + +// 404处理 +app.use((req, res) => { + if (req.path.startsWith('/api/')) { + res.status(404).json({ error: 'API接口不存在' }); + } else { + // 对于非API请求,返回index.html(用于React Router) + if (process.env.NODE_ENV === 'production') { + res.sendFile(path.join(distPath, 'index.html')); + } else { + res.sendFile(path.join(publicPath, 'index.html')); + } + } +}); + +// 错误处理中间件 +app.use((error, req, res, next) => { + console.error('服务器错误:', error); + + if (req.path.startsWith('/api/')) { + res.status(500).json({ error: '服务器内部错误' }); + } else { + res.status(500).send('服务器内部错误'); + } +}); + +// 启动服务器 +app.listen(PORT, () => { + console.log(`服务器运行在 http://localhost:${PORT}`); + console.log(`API文档: http://localhost:${PORT}/api/health`); + console.log(`环境: ${process.env.NODE_ENV || 'development'}`); +}); \ No newline at end of file diff --git a/utils/database.js b/utils/database.js new file mode 100644 index 0000000..0b87b80 --- /dev/null +++ b/utils/database.js @@ -0,0 +1,50 @@ +const Database = require('better-sqlite3'); +const path = require('path'); + +class DatabaseWrapper { + constructor() { + this.dbPath = process.env.DB_PATH || path.join(__dirname, '../data/database.sqlite'); + this.db = new Database(this.dbPath, { verbose: console.log }); + } + + // 查询单个记录 + get(sql, params = []) { + try { + const stmt = this.db.prepare(sql); + return stmt.get(params); + } catch (error) { + console.error('数据库查询错误:', error); + throw error; + } + } + + // 查询多个记录 + all(sql, params = []) { + try { + const stmt = this.db.prepare(sql); + return stmt.all(params); + } catch (error) { + console.error('数据库查询错误:', error); + throw error; + } + } + + // 执行插入、更新、删除操作 + run(sql, params = []) { + try { + const stmt = this.db.prepare(sql); + const result = stmt.run(params); + return { id: result.lastInsertRowid, changes: result.changes }; + } catch (error) { + console.error('数据库操作错误:', error); + throw error; + } + } + + // 关闭数据库连接 + close() { + this.db.close(); + } +} + +module.exports = new DatabaseWrapper(); \ No newline at end of file