nsbp-cli 0.2.21 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/bin/nsbp.js +29 -11
  3. package/package.json +2 -2
  4. package/templates/basic/Makefile +60 -18
  5. package/templates/basic/README.md +155 -3
  6. package/templates/basic/{webpack.base.js → config/webpack.base.js} +2 -2
  7. package/templates/basic/{webpack.client.js → config/webpack.client.js} +2 -2
  8. package/templates/basic/{webpack.server.js → config/webpack.server.js} +3 -3
  9. package/templates/basic/{Dockerfile → docker/Dockerfile} +2 -2
  10. package/templates/basic/{Dockerfile.dev → docker/Dockerfile.dev} +1 -1
  11. package/templates/basic/{docker-compose.dev.yml → docker/docker-compose.dev.yml} +13 -8
  12. package/templates/basic/{docker-compose.yml → docker/docker-compose.yml} +8 -4
  13. package/templates/basic/gitignore +2 -0
  14. package/templates/basic/package.json +10 -7
  15. package/templates/basic/public/apple-touch-icon.png +0 -0
  16. package/templates/basic/public/favicon-16x16.png +0 -0
  17. package/templates/basic/public/favicon-192x192.png +0 -0
  18. package/templates/basic/public/favicon-32x32.png +0 -0
  19. package/templates/basic/public/favicon-512x512.png +0 -0
  20. package/templates/basic/public/favicon.svg +10 -0
  21. package/templates/basic/public/site.webmanifest +20 -0
  22. package/templates/basic/scripts/verify-dev.sh +6 -6
  23. package/templates/basic/scripts/verify-security.sh +124 -0
  24. package/templates/basic/src/containers/Home.tsx +1 -1
  25. package/templates/basic/src/server/index.ts +56 -2
  26. package/templates/basic/src/server/utils.tsx +7 -0
  27. /package/templates/basic/{postcss.config.js → config/postcss.config.js} +0 -0
  28. /package/templates/basic/{.dockerignore → docker/.dockerignore} +0 -0
package/README.md CHANGED
@@ -147,7 +147,7 @@ node ./bin/nsbp.js --help # Test CLI locally
147
147
 
148
148
  - **Package Name**: `nsbp-cli`
149
149
  - **Bin Command**: `nsbp` (install globally and run `nsbp --help`)
150
- - **Version**: `0.2.21`
150
+ - **Version**: `0.2.24`
151
151
  - **Dependencies**: chalk, commander, fs-extra, inquirer
152
152
  - **Package Manager**: Uses pnpm (also compatible with npm)
153
153
  - **Node Version**: >=16.0.0
package/bin/nsbp.js CHANGED
@@ -21,7 +21,7 @@ program
21
21
  .command('create <project-name>')
22
22
  .description('Create a new NSBP project')
23
23
  .option('-t, --template <template>', 'Specify template (basic, blog, ecommerce)', 'basic')
24
- .option('--skip-install', 'Skip npm install')
24
+ .option('--skip-install', 'Skip pnpm install')
25
25
  .action(async (projectName, options) => {
26
26
  console.log(chalk.cyan(`🚀 Creating NSBP project: ${projectName}`));
27
27
 
@@ -104,15 +104,10 @@ program
104
104
  // Create package.json for new project
105
105
  const originalPackage = require(path.join(templateSource, 'package.json'));
106
106
  const newPackage = {
107
+ ...originalPackage,
107
108
  name: projectName,
108
109
  version: '1.0.0',
109
- description: `NSBP project: ${projectName}`,
110
- scripts: originalPackage.scripts,
111
- dependencies: originalPackage.dependencies,
112
- devDependencies: originalPackage.devDependencies,
113
- keywords: originalPackage.keywords,
114
- author: originalPackage.author,
115
- license: originalPackage.license
110
+ description: `NSBP project: ${projectName}`
116
111
  };
117
112
 
118
113
  fs.writeFileSync(
@@ -120,20 +115,43 @@ program
120
115
  JSON.stringify(newPackage, null, 2)
121
116
  );
122
117
 
118
+ // Remove package-lock.json if exists (use pnpm instead)
119
+ const packageLockPath = path.join(targetDir, 'package-lock.json');
120
+ if (fs.existsSync(packageLockPath)) {
121
+ fs.removeSync(packageLockPath);
122
+ }
123
+
124
+ // Create .npmignore to prevent package-lock.json from being committed
125
+ const npmignorePath = path.join(targetDir, '.npmignore');
126
+ if (!fs.existsSync(npmignorePath)) {
127
+ fs.writeFileSync(npmignorePath, 'package-lock.json\n');
128
+ }
129
+
123
130
  // Optionally install dependencies
124
131
  if (!options.skipInstall) {
132
+ // Check if pnpm is available
133
+ try {
134
+ execSync('which pnpm', { stdio: 'ignore' });
135
+ } catch {
136
+ console.error(chalk.red('❌ pnpm is not installed or not available in PATH.'));
137
+ console.error(chalk.yellow('Please install pnpm before continuing:'));
138
+ console.error(chalk.cyan(' npm install -g pnpm'));
139
+ console.error(chalk.yellow('Or use --skip-install flag to skip dependency installation.'));
140
+ process.exit(1);
141
+ }
142
+
125
143
  console.log(chalk.cyan('📦 Installing dependencies...'));
126
144
  process.chdir(targetDir);
127
- execSync('npm install', { stdio: 'inherit' });
145
+ execSync('pnpm install', { stdio: 'inherit' });
128
146
  }
129
147
 
130
148
  console.log(chalk.green(`✅ NSBP project "${projectName}" created successfully!`));
131
149
  console.log(chalk.yellow('\nNext steps:'));
132
150
  console.log(` cd ${projectName}`);
133
151
  if (options.skipInstall) {
134
- console.log(' npm install');
152
+ console.log(' pnpm install');
135
153
  }
136
- console.log(' npm run dev');
154
+ console.log(' pnpm run dev');
137
155
  console.log(chalk.cyan('\nHappy coding! 🎉'));
138
156
 
139
157
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsbp-cli",
3
- "version": "0.2.21",
3
+ "version": "0.2.24",
4
4
  "description": "CLI tool for creating NSBP (Node React SSR by Webpack) projects",
5
5
  "main": "index.js",
6
6
  "homepage": "https://nsbp.erishen.cn/",
@@ -17,7 +17,7 @@
17
17
  "start": "node ./bin/nsbp.js",
18
18
  "test": "echo \"Error: no test specified\" && exit 1",
19
19
  "sync-template": "node ./scripts/sync-template.js",
20
- "update": "npm run sync-template",
20
+ "update": "pnpm run sync-template",
21
21
  "update-changelog": "node ./scripts/update-changelog.js",
22
22
  "changelog": "npx conventional-changelog -p angular -i CHANGELOG.md -s"
23
23
  },
@@ -1,4 +1,4 @@
1
- .PHONY: help build dev prod down clean logs restart
1
+ .PHONY: help build dev prod down clean logs restart env-dev env-prod env-local
2
2
 
3
3
  # Package manager detection
4
4
  PNPM := $(shell which pnpm)
@@ -19,48 +19,90 @@ help: ## Show this help message
19
19
  @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
20
20
 
21
21
  build: ## Build Docker images for production
22
- $(COMPOSE) build
22
+ $(COMPOSE) -f docker/docker-compose.yml build
23
23
 
24
24
  build-dev: ## Build Docker images for development
25
- $(COMPOSE) -f docker-compose.dev.yml build
25
+ $(COMPOSE) -f docker/docker-compose.dev.yml build
26
26
 
27
27
  dev: ## Start development environment (removes orphan containers)
28
- $(COMPOSE) -f docker-compose.dev.yml up --build --remove-orphans
28
+ $(COMPOSE) -f docker/docker-compose.dev.yml up --build --remove-orphans
29
29
 
30
30
  prod: ## Start production environment (removes orphan containers)
31
- $(COMPOSE) up -d --remove-orphans
31
+ $(COMPOSE) -f docker/docker-compose.yml up -d --remove-orphans
32
32
 
33
33
  down: ## Stop and remove containers (including orphan containers)
34
- $(COMPOSE) down --remove-orphans
35
- $(COMPOSE) -f docker-compose.dev.yml down --remove-orphans
34
+ $(COMPOSE) -f docker/docker-compose.yml down --remove-orphans
35
+ $(COMPOSE) -f docker/docker-compose.dev.yml down --remove-orphans
36
36
 
37
37
  clean: ## Stop containers, remove images and volumes (including orphan containers)
38
- $(COMPOSE) down -v --rmi all --remove-orphans
39
- $(COMPOSE) -f docker-compose.dev.yml down -v --rmi all --remove-orphans
38
+ $(COMPOSE) -f docker/docker-compose.yml down -v --rmi all --remove-orphans
39
+ $(COMPOSE) -f docker/docker-compose.dev.yml down -v --rmi all --remove-orphans
40
40
 
41
41
  logs: ## View logs
42
- $(COMPOSE) logs -f
42
+ $(COMPOSE) -f docker/docker-compose.yml logs -f
43
43
 
44
44
  logs-dev: ## View development logs
45
- $(COMPOSE) -f docker-compose.dev.yml logs -f
45
+ $(COMPOSE) -f docker/docker-compose.dev.yml logs -f
46
46
 
47
47
  restart: ## Restart production containers
48
- $(COMPOSE) restart
48
+ $(COMPOSE) -f docker/docker-compose.yml restart
49
49
 
50
50
  restart-dev: ## Restart development containers
51
- $(COMPOSE) -f docker-compose.dev.yml restart
51
+ $(COMPOSE) -f docker/docker-compose.dev.yml restart
52
52
 
53
53
  rebuild: ## Rebuild and restart production containers (removes orphan containers)
54
- $(COMPOSE) up -d --build --remove-orphans
54
+ $(COMPOSE) -f docker/docker-compose.yml up -d --build --remove-orphans
55
55
 
56
56
  rebuild-dev: ## Rebuild and restart development containers (removes orphan containers)
57
- $(COMPOSE) -f docker-compose.dev.yml up -d --build --remove-orphans
57
+ $(COMPOSE) -f docker/docker-compose.dev.yml up -d --build --remove-orphans
58
58
 
59
59
  shell: ## Open shell in production container
60
- $(COMPOSE) exec app sh
60
+ $(COMPOSE) -f docker/docker-compose.yml exec app sh
61
61
 
62
62
  shell-dev: ## Open shell in development container
63
- $(COMPOSE) -f docker-compose.dev.yml exec app sh
63
+ $(COMPOSE) -f docker/docker-compose.dev.yml exec app sh
64
64
 
65
65
  test: ## Run tests (if configured)
66
- $(COMPOSE) exec app npm test
66
+ $(COMPOSE) -f docker/docker-compose.yml exec app $(PM) test
67
+
68
+ env-dev: ## Set up development environment
69
+ @if [ -f .env.development ]; then \
70
+ cp .env.development .env && \
71
+ echo -e "\033[0;32m✅ 开发环境已配置 (.env)\033[0m"; \
72
+ else \
73
+ echo -e "\033[0;33m⚠️ .env.development 不存在\033[0m"; \
74
+ fi
75
+
76
+ env-prod: ## Set up production environment
77
+ @if [ -f .env.production ]; then \
78
+ cp .env.production .env && \
79
+ echo -e "\033[0;32m✅ 生产环境已配置 (.env)\033[0m"; \
80
+ else \
81
+ echo -e "\033[0;33m⚠️ .env.production 不存在\033[0m"; \
82
+ fi
83
+
84
+ env-local: ## Set up local environment with sensitive data
85
+ @if [ ! -f .env.local ]; then \
86
+ echo "# Local environment (contains sensitive data)" > .env.local && \
87
+ echo "# DO NOT commit this file to Git" >> .env.local && \
88
+ echo -e "\033[0;32m✅ .env.local 已创建\033[0m"; \
89
+ echo "请编辑 .env.local 添加敏感信息"; \
90
+ else \
91
+ echo -e "\033[0;33m⚠️ .env.local 已存在\033[0m"; \
92
+ fi
93
+
94
+ show-env: ## Show current environment variables
95
+ @echo "========================================="
96
+ @echo "当前环境变量配置"
97
+ @echo "========================================="
98
+ @echo ""
99
+ @if [ -f .env ]; then \
100
+ echo "📄 .env 文件内容:"; \
101
+ cat .env | grep -v "^#" | grep -v "^$$"; \
102
+ else \
103
+ echo "⚠️ .env 文件不存在"; \
104
+ fi
105
+ @echo ""
106
+ @if [ -f .env.local ]; then \
107
+ echo "📄 .env.local 文件存在(包含敏感信息)"; \
108
+ fi
@@ -2,10 +2,88 @@
2
2
 
3
3
  🌐 **Online Demo**: [https://nsbp.erishen.cn/](https://nsbp.erishen.cn/)
4
4
 
5
+ ## 环境变量配置
6
+
7
+ ### 快速开始
8
+
9
+ ```bash
10
+ # 1. 复制环境变量模板
11
+ cp .env.example .env
12
+
13
+ # 2. 根据需要编辑 .env 文件
14
+ # 编辑 NODE_ENV、PORT、ENABLE_RATE_LIMIT 等
15
+
16
+ # 3. 开始开发或部署
17
+ pnpm run dev # 本地开发
18
+ docker-compose up -d # Docker 部署
19
+ ```
20
+
21
+ ### 环境变量说明
22
+
23
+ | 变量名 | 默认值 | 说明 | 推荐环境 |
24
+ |-------|--------|------|---------|
25
+ | `NODE_ENV` | development | 运行环境 (development/production) | 全部 |
26
+ | `PORT` | 3001 | 服务端口 | 全部 |
27
+ | `ENABLE_RATE_LIMIT` | 0 | 启用速率限制 (1=启用, 0=禁用) | 生产环境 |
28
+ | `DEBUG` | - | 启用调试日志 | 开发环境 |
29
+ | `TZ` | Asia/Shanghai | 时区配置 | 生产环境 |
30
+
31
+ ### 配置文件说明
32
+
33
+ - **`.env.example`** - 环境变量模板(提交到 Git)
34
+ - **`.env`** - 本地开发配置(不提交到 Git)
35
+ - **`.env.production`** - 生产环境配置(不提交到 Git)
36
+ - **`.env.development`** - 开发环境配置(不提交到 Git)
37
+ - **`.env.local`** - 本地敏感信息(最高优先级,不提交到 Git)
38
+
39
+ ### 配置优先级
40
+
41
+ ```
42
+ .env.local > .env > docker-compose.yml 默认值
43
+ ```
44
+
45
+ ### 本地开发配置
46
+
47
+ ```bash
48
+ # 复制开发环境配置
49
+ cp .env.development .env
50
+
51
+ # 或手动创建 .env 文件
52
+ cat > .env << EOF
53
+ NODE_ENV=development
54
+ PORT=3001
55
+ ENABLE_RATE_LIMIT=0
56
+ EOF
57
+
58
+ # 启动开发环境
59
+ pnpm run dev
60
+ ```
61
+
62
+ ### Docker 部署配置
63
+
64
+ ```bash
65
+ # 生产环境配置
66
+ cp .env.production .env
67
+
68
+ # Docker Compose 会自动读取 .env 文件
69
+ docker-compose up -d
70
+
71
+ # 查看环境变量是否生效
72
+ docker-compose exec app env | grep NODE_ENV
73
+ ```
74
+
75
+ ### 敏感信息管理
76
+
77
+ **重要:**
78
+ - ✅ `.env.example` 可以提交到 Git(模板文件)
79
+ - ❌ `.env`、`.env.local` 不要提交到 Git(已在 .gitignore 中)
80
+ - ✅ 敏感信息(密钥、数据库密码)放在 `.env.local` 中
81
+ - ✅ `.env.local` 会覆盖其他配置,优先级最高
82
+
5
83
  ## 开发
6
- - npm run dev (开发运行)
7
- - npm run build (生产编译)
8
- - npm start (生产运行)
84
+ - pnpm run dev (开发运行)
85
+ - pnpm run build (生产编译)
86
+ - pnpm start (生产运行)
9
87
 
10
88
  ### 本地访问
11
89
 
@@ -141,6 +219,80 @@ make rebuild-dev # 重新构建并启动开发环境
141
219
 
142
220
  - `NODE_ENV`: 运行环境 (production/development)
143
221
  - `PORT`: 服务端口 (默认 3001)
222
+ - `ENABLE_RATE_LIMIT`: 启用速率限制 (1=启用,0=禁用,默认禁用)
223
+
224
+ ## 安全特性
225
+
226
+ NSBP 内置了多层安全防护,默认启用生产级安全配置:
227
+
228
+ ### 已启用的安全措施
229
+
230
+ #### 1. **HTTP 头部安全 (Helmet)**
231
+ - Content Security Policy (CSP): 防止 XSS 攻击
232
+ - X-Frame-Options: 防止点击劫持
233
+ - X-Content-Type-Options: 防止 MIME 类型嗅探
234
+ - Strict-Transport-Security: 强制 HTTPS
235
+ - X-XSS-Protection: XSS 保护
236
+ - Referrer-Policy: 控制引用信息
237
+
238
+ #### 2. **静态文件安全**
239
+ - ✅ 禁止访问 `.env`、`.git` 等敏感文件
240
+ - ✅ 静态资源缓存优化(1 年缓存)
241
+ - ✅ 请求体大小限制(10MB)
242
+
243
+ #### 3. **技术栈隐藏**
244
+ - ✅ 移除 `X-Powered-By` 头部
245
+ - ✅ 不暴露 Express 版本信息
246
+
247
+ #### 4. **速率限制 (可选)**
248
+ - ✅ 15 分钟内最多 100 次请求
249
+ - ✅ 自动限流恶意 IP
250
+ - ✅ 可通过环境变量启用/禁用
251
+
252
+ ### 启用速率限制
253
+
254
+ 在生产环境中,建议启用速率限制以防止 DDoS 攻击:
255
+
256
+ **Docker 方式:**
257
+ ```bash
258
+ # docker-compose.yml 中添加
259
+ environment:
260
+ - ENABLE_RATE_LIMIT=1
261
+ ```
262
+
263
+ **本地开发方式:**
264
+ ```bash
265
+ # .env 文件
266
+ ENABLE_RATE_LIMIT=1
267
+
268
+ # 或命令行
269
+ ENABLE_RATE_LIMIT=1 pnpm start
270
+ ```
271
+
272
+ ### 安全最佳实践
273
+
274
+ #### 生产环境建议
275
+ 1. ✅ **启用 HTTPS**: 使用反向代理(Nginx/Apache)配置 SSL
276
+ 2. ✅ **启用速率限制**: 防止暴力攻击和 DDoS
277
+ 3. ✅ **设置强密码**: 数据库、API 密钥等
278
+ 4. ✅ **定期更新依赖**: `pnpm update`
279
+ 5. ✅ **配置防火墙**: 限制入站流量
280
+
281
+ #### 开发环境
282
+ - ✅ 默认配置已足够
283
+ - ❌ 不建议启用速率限制(影响开发效率)
284
+ - ✅ 保留详细错误日志便于调试
285
+
286
+ ### 安全检查清单
287
+
288
+ 部署前请确认:
289
+ - [ ] 已安装最新依赖 (`pnpm install`)
290
+ - [ ] 环境变量已配置(NODE_ENV=production)
291
+ - [ ] HTTPS 已配置
292
+ - [ ] 敏感信息(密钥、数据库密码)已移出代码库
293
+ - [ ] 速率限制已启用(生产环境)
294
+ - [ ] 静态文件访问已测试
295
+ - [ ] CSP 策略已测试(检查控制台错误)
144
296
 
145
297
  ### CLI 发布
146
298
 
@@ -2,7 +2,7 @@ const path = require('path')
2
2
  const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3
3
  const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
4
4
  const TerserPlugin = require('terser-webpack-plugin')
5
- const { version } = require('./package.json')
5
+ const { version } = require('../package.json')
6
6
  const LoadablePlugin = require('@loadable/webpack-plugin')
7
7
  const { createLoadableComponentsTransformer } = require('typescript-loadable-components-plugin')
8
8
  const BrowserSyncPlugin = require('browser-sync-webpack-plugin')
@@ -54,7 +54,7 @@ module.exports = ({ mode, entry, server, init }) => {
54
54
  logInfoToStdOut: true,
55
55
  logLevel: 'info',
56
56
  transpileOnly: true,
57
- configFile: path.resolve(__dirname, './tsconfig.json'),
57
+ configFile: path.resolve(__dirname, '../tsconfig.json'),
58
58
  getCustomTransformers: (program) => {
59
59
  // console.log('getCustomTransformers', program)
60
60
 
@@ -1,7 +1,7 @@
1
1
  const path = require('path') //node的path模块
2
2
  const { merge } = require('webpack-merge')
3
3
  const config = require('./webpack.base.js')
4
- const { version } = require('./package.json')
4
+ const { version } = require('../package.json')
5
5
 
6
6
  const server = false
7
7
 
@@ -12,7 +12,7 @@ const entry = {
12
12
  const clientConfig = {
13
13
  output: {
14
14
  //打包出口
15
- path: path.resolve(__dirname, 'public'),
15
+ path: path.resolve(__dirname, '../public'),
16
16
  filename: ({ chunk }) => {
17
17
  const { name } = chunk
18
18
  // console.log('name', name)
@@ -2,7 +2,7 @@ const path = require('path') //node的path模块
2
2
  const nodeExternals = require('webpack-node-externals')
3
3
  const { merge } = require('webpack-merge')
4
4
  const config = require('./webpack.base.js')
5
- const { version } = require('./package.json')
5
+ const { version } = require('../package.json')
6
6
 
7
7
  const init = process.env.INIT || 0
8
8
  const server = true
@@ -18,12 +18,12 @@ const serverConfig = {
18
18
  output: {
19
19
  //打包出口
20
20
  filename: `bundle.${version}.js`, //打包后的文件名
21
- path: path.resolve(__dirname, 'build'), //存放到根目录的build文件夹
21
+ path: path.resolve(__dirname, '../build'), //存放到根目录的build文件夹
22
22
  clean: true,
23
23
  libraryTarget: 'commonjs2'
24
24
  },
25
25
  externals: [
26
- '@loadable/component',
26
+ '@loadable/component',
27
27
  nodeExternals()
28
28
  ] //保持node中require的引用方式
29
29
  }
@@ -16,7 +16,7 @@ RUN npm install -g pnpm && pnpm install
16
16
  COPY . .
17
17
 
18
18
  # Build application
19
- RUN npm run build
19
+ RUN pnpm run build
20
20
 
21
21
  # Stage 2: Production
22
22
  FROM node:20-alpine AS production
@@ -43,7 +43,7 @@ COPY --from=builder /app/public ./public
43
43
 
44
44
  # Copy scripts and config
45
45
  COPY --from=builder /app/scripts ./scripts
46
- COPY --from=builder /app/tsconfig.json ./
46
+ COPY --from=builder /app/tsconfig.json ./tsconfig.json
47
47
 
48
48
  # Change ownership
49
49
  RUN chown -R nodejs:nodejs /app
@@ -49,4 +49,4 @@ EXPOSE 3001
49
49
  ENTRYPOINT ["/entrypoint.sh"]
50
50
 
51
51
  # Start development server
52
- CMD ["dumb-init", "--", "npm", "run", "dev"]
52
+ CMD ["dumb-init", "--", "pnpm", "run", "dev"]
@@ -1,29 +1,34 @@
1
+ # 从 .env 文件读取环境变量(如果存在)
2
+ # 开发环境配置优先级:.env > docker-compose.dev.yml 默认值
3
+
1
4
  services:
2
5
  app:
3
6
  build:
4
- context: .
5
- dockerfile: Dockerfile.dev
7
+ context: ..
8
+ dockerfile: docker/Dockerfile.dev
6
9
  container_name: nsbp-app-dev
7
10
  ports:
8
11
  - "3001:3001"
9
12
  - "9229:9229" # Node.js debug port
10
13
  environment:
11
- - NODE_ENV=development
12
- - PORT=3001
14
+ - NODE_ENV=${NODE_ENV:-development}
15
+ - PORT=${PORT:-3001}
16
+ # 开发环境默认禁用速率限制,如需启用可在 .env 中设置
17
+ - ENABLE_RATE_LIMIT=${ENABLE_RATE_LIMIT:-0}
13
18
  volumes:
14
19
  # Mount source code for hot reload
15
20
  - ./src:/app/src
16
21
  - ./public:/app/public
17
22
  - ./scripts:/app/scripts
18
- - ./webpack.base.js:/app/webpack.base.js
19
- - ./webpack.client.js:/app/webpack.client.js
20
- - ./webpack.server.js:/app/webpack.server.js
23
+ - ./config/webpack.base.js:/app/config/webpack.base.js
24
+ - ./config/webpack.client.js:/app/config/webpack.client.js
25
+ - ./config/webpack.server.js:/app/config/webpack.server.js
21
26
  - ./tsconfig.json:/app/tsconfig.json
22
27
  # Persist node_modules
23
28
  - node_modules:/app/node_modules
24
29
  # Build output - named volume for persistence
25
30
  - build_output:/app/build
26
- command: ["dumb-init", "--", "npm", "run", "dev"]
31
+ command: ["dumb-init", "--", "pnpm", "run", "dev"]
27
32
  restart: unless-stopped
28
33
  networks:
29
34
  - nsbp-network
@@ -1,14 +1,18 @@
1
+ # 从 .env 文件读取环境变量(如果存在)
2
+ # 生产环境配置优先级:.env > docker-compose.yml 默认值
3
+
1
4
  services:
2
5
  app:
3
6
  build:
4
- context: .
5
- dockerfile: Dockerfile
7
+ context: ..
8
+ dockerfile: docker/Dockerfile
6
9
  container_name: nsbp-app
7
10
  ports:
8
11
  - "8091:3001"
9
12
  environment:
10
- - NODE_ENV=production
11
- - PORT=3001
13
+ - NODE_ENV=${NODE_ENV:-production}
14
+ - PORT=${PORT:-3001}
15
+ - ENABLE_RATE_LIMIT=${ENABLE_RATE_LIMIT:-1}
12
16
  restart: unless-stopped
13
17
  healthcheck:
14
18
  test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
@@ -23,6 +23,8 @@ public/*.json
23
23
  .env
24
24
  .env.local
25
25
  .env.*.local
26
+ .env.development
27
+ .env.production
26
28
 
27
29
  # Logs
28
30
  logs
@@ -3,16 +3,16 @@
3
3
  "version": "1.0.0",
4
4
  "description": "node react ssr by webpack",
5
5
  "main": "index.js",
6
- "homepage": "https://nsbp.erishen.cn/",
6
+ "homepage": "https://nsbp.erishen.cn",
7
7
  "scripts": {
8
8
  "dev": "npm-run-all -l -s dev:init -p dev:build:*",
9
- "dev:init": "cross-env INIT=1 webpack --config webpack.server.js --mode development",
10
- "dev:build:server": "webpack --config webpack.server.js --mode development --watch",
11
- "dev:build:client": "webpack --config webpack.client.js --mode development --watch",
9
+ "dev:init": "cross-env INIT=1 webpack --config config/webpack.server.js --mode development",
10
+ "dev:build:server": "webpack --config config/webpack.server.js --mode development --watch",
11
+ "dev:build:client": "webpack --config config/webpack.client.js --mode development --watch",
12
12
  "dev:build:start": "node ./scripts/start.js",
13
- "build": "npm run clean && npm-run-all -l -p build:**",
14
- "build:server": "webpack --config webpack.server.js --mode production",
15
- "build:client": "webpack --config webpack.client.js --mode production",
13
+ "build": "pnpm run clean && npm-run-all -l -p build:**",
14
+ "build:server": "webpack --config config/webpack.server.js --mode production",
15
+ "build:client": "webpack --config config/webpack.client.js --mode production",
16
16
  "start": "node ./scripts/start.js",
17
17
  "clean": "rimraf build && rimraf public/js && rimraf public/css && rimraf public/client.* && rimraf public/*.js && rimraf public/*.js.map && rimraf public/*.txt && rimraf public/*.json && rimraf .temp_cache",
18
18
  "format": "prettier --write **/*.{js,css,less,scss,ts,tsx}",
@@ -29,6 +29,7 @@
29
29
  ],
30
30
  "author": "Erishen Sun",
31
31
  "license": "ISC",
32
+ "packageManager": "pnpm@10.27.0",
32
33
  "dependencies": {
33
34
  "@loadable/component": "^5.15.0",
34
35
  "@loadable/server": "^5.15.0",
@@ -36,7 +37,9 @@
36
37
  "@reduxjs/toolkit": "^2.11.2",
37
38
  "axios": "^1.7.0",
38
39
  "express": "^5.2.1",
40
+ "express-rate-limit": "^8.2.0",
39
41
  "framer-motion": "^12.25.0",
42
+ "helmet": "^8.1.0",
40
43
  "lodash": "^4.17.21",
41
44
  "probe-image-size": "^7.1.0",
42
45
  "react": "^19.2.3",
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <defs>
3
+ <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#2563eb" />
5
+ <stop offset="100%" stop-color="#1d4ed8" />
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="100" height="100" rx="20" fill="url(#gradient)" />
9
+ <text x="50" y="68" font-family="Arial, sans-serif" font-size="60" font-weight="bold" text-anchor="middle" fill="white">N</text>
10
+ </svg>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "Nsbp.js - 轻量级 React SSR 框架",
3
+ "short_name": "Nsbp.js",
4
+ "description": "轻量级 React SSR 框架,专为低资源部署与高度可定制场景而生",
5
+ "icons": [
6
+ {
7
+ "src": "/favicon-192x192.png",
8
+ "sizes": "192x192",
9
+ "type": "image/png"
10
+ },
11
+ {
12
+ "src": "/favicon-512x512.png",
13
+ "sizes": "512x512",
14
+ "type": "image/png"
15
+ }
16
+ ],
17
+ "theme_color": "#2563eb",
18
+ "background_color": "#ffffff",
19
+ "display": "standalone"
20
+ }
@@ -20,7 +20,7 @@ else
20
20
  echo ""
21
21
  echo "启动命令:"
22
22
  echo " make dev"
23
- echo " docker-compose -f docker-compose.dev.yml up -d --build"
23
+ echo " docker-compose -f docker/docker-compose.dev.yml up -d --build"
24
24
  exit 1
25
25
  fi
26
26
 
@@ -35,7 +35,7 @@ fi
35
35
 
36
36
  echo ""
37
37
  echo "3. 检查权限错误..."
38
- if docker-compose -f docker-compose.dev.yml logs 2>&1 | grep -q "EACCES"; then
38
+ if docker-compose -f docker/docker-compose.dev.yml logs 2>&1 | grep -q "EACCES"; then
39
39
  echo -e "${RED}❌ 发现权限错误${NC}"
40
40
  echo ""
41
41
  echo "最近的权限错误:"
@@ -46,7 +46,7 @@ fi
46
46
 
47
47
  echo ""
48
48
  echo "4. 检查构建状态..."
49
- if docker-compose -f docker-compose.dev.yml logs 2>&1 | grep -q "compiled successfully"; then
49
+ if docker-compose -f docker/docker-compose.dev.yml logs 2>&1 | grep -q "compiled successfully"; then
50
50
  echo -e "${GREEN}✅ 构建完成${NC}"
51
51
  else
52
52
  echo -e "${YELLOW}⚠️ 构建中或未开始${NC}"
@@ -54,7 +54,7 @@ fi
54
54
 
55
55
  echo ""
56
56
  echo "5. 检查服务器监听..."
57
- if docker-compose -f docker-compose.dev.yml logs 2>&1 | grep -q "listening"; then
57
+ if docker-compose -f docker/docker-compose.dev.yml logs 2>&1 | grep -q "listening"; then
58
58
  echo -e "${GREEN}✅ 服务器正在监听${NC}"
59
59
  else
60
60
  echo -e "${YELLOW}⚠️ 服务器尚未启动监听${NC}"
@@ -69,9 +69,9 @@ echo "访问应用:"
69
69
  echo " http://localhost:3001"
70
70
  echo ""
71
71
  echo "查看日志:"
72
- echo " docker-compose -f docker-compose.dev.yml logs -f"
72
+ echo " docker-compose -f docker/docker-compose.dev.yml logs -f"
73
73
  echo ""
74
74
  echo "停止服务:"
75
75
  echo " make down"
76
- echo " docker-compose -f docker-compose.dev.yml down"
76
+ echo " docker-compose -f docker/docker-compose.dev.yml down"
77
77
  echo ""
@@ -0,0 +1,124 @@
1
+ #!/bin/bash
2
+
3
+ echo "========================================="
4
+ echo "NSBP 安全功能验证"
5
+ echo "========================================="
6
+ echo ""
7
+
8
+ GREEN='\033[0;32m'
9
+ RED='\033[0;31m'
10
+ YELLOW='\033[1;33m'
11
+ NC='\033[0m'
12
+
13
+ # 检查依赖
14
+ echo "1. 检查安全依赖安装..."
15
+ if grep -q '"helmet"' package.json && grep -q '"express-rate-limit"' package.json; then
16
+ echo -e "${GREEN}✅ helmet 和 express-rate-limit 已安装${NC}"
17
+ else
18
+ echo -e "${RED}❌ 缺少安全依赖${NC}"
19
+ exit 1
20
+ fi
21
+
22
+ echo ""
23
+
24
+ # 检查服务器代码
25
+ echo "2. 检查服务器代码安全配置..."
26
+ if grep -q "helmet(" src/server/index.ts; then
27
+ echo -e "${GREEN}✅ Helmet 已启用${NC}"
28
+ else
29
+ echo -e "${RED}❌ Helmet 未配置${NC}"
30
+ fi
31
+
32
+ if grep -q "rateLimit" src/server/index.ts; then
33
+ echo -e "${GREEN}✅ 速率限制已配置(可选)${NC}"
34
+ else
35
+ echo -e "${RED}❌ 速率限制未配置${NC}"
36
+ fi
37
+
38
+ if grep -q "dotfiles: 'ignore'" src/server/index.ts; then
39
+ echo -e "${GREEN}✅ dotfiles 访问已禁用${NC}"
40
+ else
41
+ echo -e "${RED}⚠️ dotfiles 配置需要检查${NC}"
42
+ fi
43
+
44
+ if grep -q "disable('x-powered-by')" src/server/index.ts; then
45
+ echo -e "${GREEN}✅ X-Powered-By 已隐藏${NC}"
46
+ else
47
+ echo -e "${RED}❌ X-Powered-By 未隐藏${NC}"
48
+ fi
49
+
50
+ if grep -q "express.json.*limit" src/server/index.ts && grep -q "express.urlencoded.*limit" src/server/index.ts; then
51
+ echo -e "${GREEN}✅ 请求体大小限制已配置${NC}"
52
+ else
53
+ echo -e "${RED}❌ 请求体大小限制未配置${NC}"
54
+ fi
55
+
56
+ echo ""
57
+
58
+ # 检查 CSP 配置
59
+ echo "3. 检查 Content Security Policy..."
60
+ if grep -q "contentSecurityPolicy" src/server/index.ts; then
61
+ echo -e "${GREEN}✅ CSP 已配置${NC}"
62
+ if grep -q "defaultSrc.*'self'" src/server/index.ts; then
63
+ echo -e "${GREEN} - default-src: 'self'${NC}"
64
+ fi
65
+ if grep -q "scriptSrc" src/server/index.ts; then
66
+ echo -e "${GREEN} - script-src 已配置${NC}"
67
+ fi
68
+ if grep -q "styleSrc" src/server/index.ts; then
69
+ echo -e "${GREEN} - style-src 已配置${NC}"
70
+ fi
71
+ if grep -q "imgSrc.*https:" src/server/index.ts; then
72
+ echo -e "${GREEN} - img-src 允许 HTTPS${NC}"
73
+ fi
74
+ else
75
+ echo -e "${RED}❌ CSP 未配置${NC}"
76
+ fi
77
+
78
+ echo ""
79
+
80
+ # 检查文档
81
+ echo "4. 检查安全文档..."
82
+ if grep -q "## 安全特性" README.md; then
83
+ echo -e "${GREEN}✅ 安全章节已添加到 README${NC}"
84
+ else
85
+ echo -e "${YELLOW}⚠️ 建议添加安全章节到 README${NC}"
86
+ fi
87
+
88
+ echo ""
89
+
90
+ # 统计
91
+ echo "========================================="
92
+ echo "安全配置统计"
93
+ echo "========================================="
94
+
95
+ TOTAL_CHECKS=7
96
+ PASSED_CHECKS=0
97
+
98
+ # 统计通过项
99
+ grep -q '"helmet"' package.json && grep -q '"express-rate-limit"' package.json && ((PASSED_CHECKS++))
100
+ grep -q "helmet(" src/server/index.ts && ((PASSED_CHECKS++))
101
+ grep -q "dotfiles: 'ignore'" src/server/index.ts && ((PASSED_CHECKS++))
102
+ grep -q "disable('x-powered-by')" src/server/index.ts && ((PASSED_CHECKS++))
103
+ grep -q "express.json.*limit" src/server/index.ts && ((PASSED_CHECKS++))
104
+ grep -q "contentSecurityPolicy" src/server/index.ts && ((PASSED_CHECKS++))
105
+ grep -q "## 安全特性" README.md && ((PASSED_CHECKS++))
106
+
107
+ SCORE=$(( PASSED_CHECKS * 100 / TOTAL_CHECKS ))
108
+
109
+ echo -e "通过率: ${PASSED_CHECKS}/${TOTAL_CHECKS} (${YELLOW}${SCORE}%${NC})"
110
+ echo ""
111
+
112
+ if [ $SCORE -eq 100 ]; then
113
+ echo -e "${GREEN}🎉 所有安全检查通过!${NC}"
114
+ echo "项目已达到生产级安全标准。"
115
+ elif [ $SCORE -ge 70 ]; then
116
+ echo -e "${YELLOW}✅ 安全配置良好${NC}"
117
+ echo "建议检查未通过的项目。"
118
+ else
119
+ echo -e "${RED}⚠️ 安全配置需要改进${NC}"
120
+ echo "请检查上述未通过的项目。"
121
+ fi
122
+
123
+ echo ""
124
+ echo "========================================="
@@ -369,7 +369,7 @@ export const getPhotoMenu = (req: any, res: any) => {
369
369
 
370
370
  <QuickStartCard>
371
371
  <QuickStartTitle>2️⃣ 启动开发</QuickStartTitle>
372
- <QuickStartCode>$ npm run dev</QuickStartCode>
372
+ <QuickStartCode>$ pnpm run dev</QuickStartCode>
373
373
  <QuickStartDescription>
374
374
  启动开发服务器,默认端口 3001
375
375
  </QuickStartDescription>
@@ -1,12 +1,62 @@
1
1
  import express from 'express'
2
+ import helmet from 'helmet'
3
+ import rateLimit from 'express-rate-limit'
2
4
  import { render } from './utils'
3
5
  import { getPhotoWH, getPhotoMenu } from './photo'
4
6
  import { useCurrentFlag, outPhotoDicPath } from '../utils/config'
5
7
 
6
8
  const app = express()
7
9
 
8
- app.use(express.static('public', { dotfiles: 'allow' }))
9
- !useCurrentFlag && app.use(express.static(outPhotoDicPath, { dotfiles: 'allow' }))
10
+ // 1. Security headers (helmet)
11
+ app.use(helmet({
12
+ contentSecurityPolicy: {
13
+ directives: {
14
+ defaultSrc: ["'self'"],
15
+ scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
16
+ styleSrc: ["'self'", "'unsafe-inline'"],
17
+ imgSrc: ["'self'", "data:", "https:"],
18
+ connectSrc: ["'self'", "https:"],
19
+ fontSrc: ["'self'", "data:"],
20
+ objectSrc: ["'none'"],
21
+ mediaSrc: ["'self'"],
22
+ frameSrc: ["'none'"],
23
+ },
24
+ },
25
+ crossOriginEmbedderPolicy: false, // Allow inline scripts for SSR
26
+ crossOriginOpenerPolicy: false, // Allow window.open for development
27
+ }))
28
+
29
+ // 2. Hide X-Powered-By header
30
+ app.disable('x-powered-by')
31
+
32
+ // 3. Rate limiting (optional, controlled by environment variable)
33
+ if (process.env.ENABLE_RATE_LIMIT === '1') {
34
+ const limiter = rateLimit({
35
+ windowMs: 15 * 60 * 1000, // 15 minutes
36
+ max: 100, // Limit each IP to 100 requests per windowMs
37
+ message: 'Too many requests from this IP, please try again later.',
38
+ standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
39
+ legacyHeaders: false, // Disable the `X-RateLimit-*` headers
40
+ })
41
+ app.use('/api', limiter)
42
+ console.log('🛡️ Rate limiting enabled for /api routes')
43
+ }
44
+
45
+ // 4. Static file serving (disable dotfiles access)
46
+ app.use(express.static('public', {
47
+ dotfiles: 'ignore',
48
+ setHeaders: (res, filePath) => {
49
+ // Cache static assets for 1 year
50
+ if (filePath.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
51
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
52
+ }
53
+ }
54
+ }))
55
+ !useCurrentFlag && app.use(express.static(outPhotoDicPath, { dotfiles: 'ignore' }))
56
+
57
+ // 5. Body parsing with size limits
58
+ app.use(express.json({ limit: '10mb' }))
59
+ app.use(express.urlencoded({ limit: '10mb', extended: true }))
10
60
 
11
61
  //使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹
12
62
 
@@ -27,4 +77,8 @@ app.use((req, res) => {
27
77
  const PORT = process.env.PORT || 3001
28
78
  app.listen(PORT, () => {
29
79
  console.log(`Server listening on port ${PORT}`)
80
+ console.log(`🔒 Security headers enabled`)
81
+ if (process.env.ENABLE_RATE_LIMIT === '1') {
82
+ console.log(`🚦 Rate limiting active`)
83
+ }
30
84
  })
@@ -116,6 +116,13 @@ export const render = (req: any, res: any) => {
116
116
  <html>
117
117
  <head>
118
118
  <meta charset="utf-8">
119
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
120
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
121
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
122
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
123
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
124
+ <link rel="manifest" href="/site.webmanifest">
125
+ <meta name="theme-color" content="#2563eb">
119
126
  ${helmet?.title?.toString()}
120
127
  ${helmet?.meta?.toString()}
121
128
  ${styleTags}