create-ssnode-cli 0.1.0 → 0.2.2

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 (107) hide show
  1. package/README.md +1 -2
  2. package/index.js +10 -4
  3. package/package.json +2 -2
  4. package/secrets.js +10 -7
  5. package/templates/backend/Dockerfile +4 -0
  6. package/templates/backend/config/config.develop.yaml +10 -0
  7. package/templates/backend/config/config.prod.yaml +10 -0
  8. package/templates/backend/dist/config/configuration.js +1 -1
  9. package/templates/backend/dist/config/configuration.js.map +1 -1
  10. package/templates/backend/dist/modules/auth/auth.service.js +1 -1
  11. package/templates/backend/dist/modules/auth/auth.service.js.map +1 -1
  12. package/templates/backend/dist/modules/page-data/page-data.controller.js +1 -1
  13. package/templates/backend/dist/modules/page-data/page-data.controller.js.map +1 -1
  14. package/templates/backend/dist/modules/screen-config/screen-config.controller.js +1 -1
  15. package/templates/backend/dist/modules/screen-config/screen-config.controller.js.map +1 -1
  16. package/templates/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  17. package/templates/backend/node_modules/.bin/js-yaml +2 -2
  18. package/templates/backend/node_modules/.bin/typeorm +2 -2
  19. package/templates/backend/node_modules/.bin/typeorm-ts-node-commonjs +2 -2
  20. package/templates/backend/node_modules/.bin/typeorm-ts-node-esm +2 -2
  21. package/templates/backend/ops/docker/.env.example +4 -0
  22. package/templates/backend/ops/docker/config.sh +2 -0
  23. package/templates/backend/ops/docker/install.sh +35 -0
  24. package/templates/backend/ops/install.sh +17 -3
  25. package/templates/backend/ops/pm2/config.sh +2 -0
  26. package/templates/backend/ops/systemd/config.sh +2 -0
  27. package/templates/backend/package.json +13 -6
  28. package/templates/backend/scripts/build.cjs +67 -1
  29. package/templates/backend/src/config/config.interface.ts +9 -0
  30. package/templates/backend/src/config/configuration.ts +11 -0
  31. package/templates/backend/src/modules/auth/auth.service.ts +57 -12
  32. package/templates/backend/src/modules/page-data/page-data.controller.ts +33 -10
  33. package/templates/backend/src/modules/screen-config/screen-config.controller.ts +33 -5
  34. package/templates/root/AGENTS.md +1 -1
  35. package/templates/root/README.md +9 -6
  36. package/templates/root/package.json +9 -1
  37. package/templates/web/dist/assets/{404-f80ef771.js → 404-e6edada0.js} +1 -1
  38. package/templates/web/dist/assets/{404-legacy-3002ee98.js → 404-legacy-f6ca5b91.js} +1 -1
  39. package/templates/web/dist/assets/AuthLayout-bcf98efb.js +1 -0
  40. package/templates/web/dist/assets/AuthLayout-legacy-6e7a8ae1.js +1 -0
  41. package/templates/web/dist/assets/Background-90a49ee7.css +1 -0
  42. package/templates/web/dist/assets/Background-eb77ef54.js +1 -0
  43. package/templates/web/dist/assets/Background-legacy-50457007.js +1 -0
  44. package/templates/web/dist/assets/{Screen-2126fd8d.js → Screen-318c3fc2.js} +1 -1
  45. package/templates/web/dist/assets/{Screen-legacy-e1974f1b.js → Screen-legacy-af123b0f.js} +1 -1
  46. package/templates/web/dist/assets/{ScreenChartOption-cc9d0192.js → ScreenChartOption-953b9157.js} +1 -1
  47. package/templates/web/dist/assets/{ScreenChartOption-legacy-b1b2d49d.js → ScreenChartOption-legacy-2e8fcfd8.js} +1 -1
  48. package/templates/web/dist/assets/api-beff4a3e.js +1 -0
  49. package/templates/web/dist/assets/api-legacy-9fe41d4b.js +1 -0
  50. package/templates/web/dist/assets/{base-8cbac341.js → base-a14309c8.js} +1 -1
  51. package/templates/web/dist/assets/{base-legacy-ee9fb316.js → base-legacy-2bc6e427.js} +1 -1
  52. package/templates/web/dist/assets/{child-10cea360.js → child-2a48ca65.js} +1 -1
  53. package/templates/web/dist/assets/{child-legacy-5af47aba.js → child-legacy-7665b5f8.js} +1 -1
  54. package/templates/web/dist/assets/{component-ae0df581.js → component-89b5253e.js} +1 -1
  55. package/templates/web/dist/assets/{component-legacy-7824b5dc.js → component-legacy-8c5a28ef.js} +1 -1
  56. package/templates/web/dist/assets/{demo-1b5e36be.js → demo-d7272cf4.js} +1 -1
  57. package/templates/web/dist/assets/{demo-legacy-e12d2ac9.js → demo-legacy-f7bbfe55.js} +1 -1
  58. package/templates/web/dist/assets/echarts-vendor-50090982.js +9 -0
  59. package/templates/web/dist/assets/echarts-vendor-legacy-50b58722.js +10 -0
  60. package/templates/web/dist/assets/{grandson-d5883b90.js → grandson-05f8bb58.js} +1 -1
  61. package/templates/web/dist/assets/{grandson-legacy-10cd2476.js → grandson-legacy-3e10990b.js} +1 -1
  62. package/templates/web/dist/assets/{index-9fbd51da.js → index-466a1ec1.js} +1 -1
  63. package/templates/web/dist/assets/{index-5dbae75a.js → index-5b5a8146.js} +1 -1
  64. package/templates/web/dist/assets/{index-6faaa6d8.js → index-7277cbfb.js} +1 -1
  65. package/templates/web/dist/assets/{index-8f6fba9f.js → index-777ef781.js} +1 -1
  66. package/templates/web/dist/assets/{index-2e42b180.js → index-86214b19.js} +1 -1
  67. package/templates/web/dist/assets/{index-463d9205.js → index-9cd5622a.js} +1 -1
  68. package/templates/web/dist/assets/{index-16f6b85c.js → index-a79e448b.js} +1 -1
  69. package/templates/web/dist/assets/{index-3a9a0a97.js → index-b60badf7.js} +1 -1
  70. package/templates/web/dist/assets/{index-fbf4ee3f.js → index-bd4ed808.js} +1 -1
  71. package/templates/web/dist/assets/{index-687d6cfe.js → index-cba19800.js} +1 -1
  72. package/templates/web/dist/assets/{index-5dbcad00.js → index-d70eb540.js} +1 -1
  73. package/templates/web/dist/assets/{index-25ea283a.js → index-e6071630.js} +1 -1
  74. package/templates/web/dist/assets/{index-legacy-ced5a7f1.js → index-legacy-028ed5d5.js} +1 -1
  75. package/templates/web/dist/assets/{index-legacy-8c7d1ee7.js → index-legacy-06483318.js} +1 -1
  76. package/templates/web/dist/assets/{index-legacy-ba034322.js → index-legacy-0da8de82.js} +1 -1
  77. package/templates/web/dist/assets/{index-legacy-3d221bb6.js → index-legacy-3965dcaf.js} +1 -1
  78. package/templates/web/dist/assets/{index-legacy-f3e49a4e.js → index-legacy-4a13dddc.js} +1 -1
  79. package/templates/web/dist/assets/{index-legacy-e9af81d8.js → index-legacy-783129a7.js} +1 -1
  80. package/templates/web/dist/assets/{index-legacy-07d00151.js → index-legacy-8a2cfb9d.js} +1 -1
  81. package/templates/web/dist/assets/{index-legacy-dd62c9e7.js → index-legacy-993ef005.js} +1 -1
  82. package/templates/web/dist/assets/{index-legacy-38c170dc.js → index-legacy-b075295d.js} +1 -1
  83. package/templates/web/dist/assets/{index-legacy-95da3338.js → index-legacy-b3bd40c1.js} +1 -1
  84. package/templates/web/dist/assets/{index-legacy-9be704da.js → index-legacy-d510e318.js} +1 -1
  85. package/templates/web/dist/assets/{index-legacy-0f01787c.js → index-legacy-feb54578.js} +1 -1
  86. package/templates/web/dist/assets/{parent-e7114c1b.js → parent-bba709aa.js} +1 -1
  87. package/templates/web/dist/assets/{parent-legacy-9e493d04.js → parent-legacy-48111978.js} +1 -1
  88. package/templates/web/dist/assets/vendor-affac96c.js +124 -0
  89. package/templates/web/dist/assets/vendor-legacy-55fa76eb.js +122 -0
  90. package/templates/web/dist/index.html +3 -3
  91. package/templates/web/package.json +7 -7
  92. package/templates/web/src/js/xlsl.ts +110 -59
  93. package/templates/web/src/layouts/AuthLayout.tsx +16 -11
  94. package/templates/web/src/layouts/Background.module.less +5 -3
  95. package/templates/web/src/services/api/admin/user.ts +9 -2
  96. package/templates/web/dist/assets/AuthLayout-f07f2dff.js +0 -1
  97. package/templates/web/dist/assets/AuthLayout-legacy-635887dd.js +0 -1
  98. package/templates/web/dist/assets/Background-63ce83a2.js +0 -1
  99. package/templates/web/dist/assets/Background-b264d337.css +0 -1
  100. package/templates/web/dist/assets/Background-legacy-a34b521f.js +0 -1
  101. package/templates/web/dist/assets/api-21ca288f.js +0 -1
  102. package/templates/web/dist/assets/api-legacy-b464070b.js +0 -1
  103. package/templates/web/dist/assets/echarts-vendor-fdb2c14f.js +0 -9
  104. package/templates/web/dist/assets/echarts-vendor-legacy-e99e5121.js +0 -10
  105. package/templates/web/dist/assets/vendor-e7ea3a11.js +0 -96
  106. package/templates/web/dist/assets/vendor-legacy-a62a28c9.js +0 -87
  107. package/templates/web/node_modules/.bin/xlsx +0 -17
@@ -6,9 +6,9 @@ case `uname` in
6
6
  esac
7
7
 
8
8
  if [ -z "$NODE_PATH" ]; then
9
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/bin/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
9
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/bin/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
10
10
  else
11
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/bin/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules/js-yaml/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.1.0/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
11
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/bin/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules/js-yaml/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/js-yaml@4.3.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
12
12
  fi
13
13
  if [ -x "$basedir/node" ]; then
14
14
  exec "$basedir/node" "$basedir/../js-yaml/bin/js-yaml.js" "$@"
@@ -6,9 +6,9 @@ case `uname` in
6
6
  esac
7
7
 
8
8
  if [ -z "$NODE_PATH" ]; then
9
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
9
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
10
10
  else
11
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
11
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
12
12
  fi
13
13
  if [ -x "$basedir/node" ]; then
14
14
  exec "$basedir/node" "$basedir/../typeorm/cli.js" "$@"
@@ -6,9 +6,9 @@ case `uname` in
6
6
  esac
7
7
 
8
8
  if [ -z "$NODE_PATH" ]; then
9
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
9
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
10
10
  else
11
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
11
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
12
12
  fi
13
13
  if [ -x "$basedir/node" ]; then
14
14
  exec "$basedir/node" "$basedir/../typeorm/cli-ts-node-commonjs.js" "$@"
@@ -6,9 +6,9 @@ case `uname` in
6
6
  esac
7
7
 
8
8
  if [ -z "$NODE_PATH" ]; then
9
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
9
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules"
10
10
  else
11
- export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.20_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
11
+ export NODE_PATH="/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules/typeorm/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/typeorm@0.3.31_better-sqlite3@13.0.3_ts-node@10.9.2/node_modules:/Users/sunset/Desktop/YunZhiHuiProgram/fullstack-template/node_modules/.pnpm/node_modules:$NODE_PATH"
12
12
  fi
13
13
  if [ -x "$basedir/node" ]; then
14
14
  exec "$basedir/node" "$basedir/../typeorm/cli-ts-node-esm.js" "$@"
@@ -32,3 +32,7 @@ LOG_LEVEL=warn
32
32
  ADMIN_PASSWORD=
33
33
  APP_SIGN_KEY=
34
34
  APP_SIGN_TS_WINDOW=300
35
+ # 单点登录(默认关闭;接入时改 true 并配 ticketKey,见 README「单点登录」)
36
+ SSO_ENABLED=false
37
+ SSO_TICKET_KEY=
38
+ SSO_TS_WINDOW=300
@@ -33,6 +33,8 @@ CONTAINER_PORT="$PORT"
33
33
  CONTAINER_DATA_DIR="${CONTAINER_DATA_DIR:-/app/data}"
34
34
  # 重启策略
35
35
  RESTART_POLICY="${RESTART_POLICY:-unless-stopped}"
36
+ # 版本轮转保留数(docker = 本镜像仓库历史 tag 数,即回滚窗口;顶层 install.sh 轮转时读)
37
+ KEEP_VERSIONS="${KEEP_VERSIONS:-3}"
36
38
 
37
39
  # 读 current 的 version + .build-ts 作镜像 tag(<version>-<14位ts>,与 systemd/pm2 releases/<ver>-<ts> 同口径)。
38
40
  # .build-ts 由 buildops 写入;缺则回退纯 version(兼容旧 release,但同 version 重发会覆盖,留意回滚)。
@@ -67,4 +67,39 @@ docker run -d \
67
67
  "$IMAGE_TAG"
68
68
 
69
69
  echo "✓ 容器已起: $CONTAINER_NAME (宿主 $HOST_PORT → $CONTAINER_PORT, tag $TAG)"
70
+
71
+ # ── 镜像轮转:对齐 systemd/pm2 的 rotate_releases 口径 ──
72
+ # docker 模式版本靠镜像 tag、不进 releases/,故轮转对象 = 本镜像仓库的历史 tag:
73
+ # 按末段 14 位时间戳降序保留 KEEP_VERSIONS 个(缺省见 config.sh),其余 rmi(回滚窗口=保留数)。
74
+ # KEEP_VERSIONS 优先级:顶层 --keep/env 显式 export > config.sh 持久值(运维改这处)。
75
+ # 不删运行中容器在用的 tag(防御);versionswitch.sh 切版本不经过本脚本,不受影响。
76
+ rotate_images() {
77
+ # 半成品兜底:经典 builder 多阶段构建的 deps 阶段成品(711MB 级 <none>)无 tag、无子层,轮转清不到;
78
+ # 按 Dockerfile 烙进各层的 app label 定向 prune(仅本应用,不碰宿主其他镜像)。
79
+ # 但该 <none> 同时是经典 builder 的构建缓存(删掉 = 下次全量重跑 apt+npm ci),故加 until=72h:
80
+ # 只清 72h 前的陈年残留,当天/周末内刚生成的缓存保住(发版即刷新存活期);放在 tag 轮转前,
81
+ # 不受 ≤KEEP_VERSIONS 提前 return 影响。
82
+ docker image prune -f --filter "label=app=$IMAGE" --filter "until=72h" >/dev/null 2>&1 || true
83
+ local running_ref
84
+ running_ref="$(docker inspect "$CONTAINER_NAME" --format '{{.Config.Image}}' 2>/dev/null || true)"
85
+ local list=() t
86
+ # tag 形如 <version>-<14位ts>;awk 提取末段时间戳做 sort 键降序(与顶层 sort_releases 同构)
87
+ while IFS= read -r t; do
88
+ [[ -n "$t" ]] || continue
89
+ list+=("$t")
90
+ done < <(docker images "$IMAGE" --format '{{.Tag}}' | grep -E -- '-[0-9]{14}$' \
91
+ | awk '{ts=$0; sub(/.*-/,"",ts); print ts" "$0}' | sort -r | awk '{print $2}')
92
+ [[ ${#list[@]} -le $KEEP_VERSIONS ]] && return 0
93
+ local del=("${list[@]:$KEEP_VERSIONS}")
94
+ for t in "${del[@]}"; do
95
+ [[ -n "$t" ]] || continue
96
+ [[ "$IMAGE:$t" = "$running_ref" ]] && continue
97
+ docker rmi "$IMAGE:$t" >/dev/null 2>&1 \
98
+ && echo " ↳ 清理旧镜像: $IMAGE:$t" \
99
+ || echo " ⚠ rmi 失败(可能被退出容器引用,清理残留容器后重试): $IMAGE:$t"
100
+ done
101
+ echo "✓ 镜像轮转:保留 $KEEP_VERSIONS 个 tag"
102
+ }
103
+ rotate_images
104
+
70
105
  echo " 状态: ./status.sh | 日志: ./logs.sh"
@@ -31,7 +31,9 @@ set -euo pipefail
31
31
  # ── baked 字段(首次为空;bootstrap 拷自身到 APP_ROOT 时 sed 改写为字面量) ──
32
32
  APP_ROOT="${APP_ROOT:-}"
33
33
  MODE="${MODE:-}"
34
- KEEP_VERSIONS="${KEEP_VERSIONS:-3}"
34
+ # 轮转保留数:此处只存显式值(--keep / env),不预设缺省 —— 缺省在轮转前由
35
+ # resolve_keep_versions 从 ops/<mode>/config.sh 取(运维改那处,跨版本保留)。
36
+ KEEP_VERSIONS="${KEEP_VERSIONS:-}"
35
37
 
36
38
  # 脚本所在目录(首次=release 包根,即 install.sh 所在的包根;二次=APP_ROOT)
37
39
  DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -417,12 +419,24 @@ dispatch_install() {
417
419
  local mode_install="$APP_ROOT/ops/$MODE/install.sh" # 共享 ops(releases 不放 ops)
418
420
  [[ -f "$mode_install" ]] || die "缺 mode 安装脚本: $mode_install(先跑 lay_out_ops 或检查后台服务目录/ops)"
419
421
  say "派发 $MODE 安装: $mode_install"
420
- export APP_ROOT MODE
422
+ export APP_ROOT MODE KEEP_VERSIONS
421
423
  bash "$mode_install"
422
424
  }
423
425
 
426
+ # ── 轮转保留数解析:--keep/env 显式 > ops/<mode>/config.sh 持久值 > 3 ──
427
+ # KEEP_VERSIONS 不 bake(区别 APP_ROOT/MODE):运维的保留数放 mode config.sh,
428
+ # lay_out_ops 跨版本保留(不 overlay);--keep 仅当次部署生效。
429
+ # source 无名冲突(config.sh 只设 mode 专属变量 + APP_ROOT 校验,顶层变量不被覆盖)。
430
+ resolve_keep_versions() {
431
+ [[ -n "$KEEP_VERSIONS" ]] && return 0
432
+ local cfg="$APP_ROOT/ops/$MODE/config.sh"
433
+ if [[ -f "$cfg" ]]; then source "$cfg"; fi # bootstrap 时 lay_out_ops 已先于轮转就位
434
+ KEEP_VERSIONS="${KEEP_VERSIONS:-3}" # 旧部署 config.sh 无此项 → 回落 3(与历史行为一致)
435
+ }
436
+
424
437
  rotate_releases() {
425
- [[ "$IS_SYMLINK" = 0 ]] && return 0 # docker 不用 releases
438
+ resolve_keep_versions
439
+ [[ "$IS_SYMLINK" = 0 ]] && return 0 # docker 不用 releases(镜像轮转在 ops/docker/install.sh::rotate_images)
426
440
  local list=()
427
441
  while IFS= read -r d; do list+=("$d"); done < <(sort_releases "$APP_ROOT/releases")
428
442
  [[ ${#list[@]} -le $KEEP_VERSIONS ]] && return 0
@@ -16,6 +16,8 @@ APP_ROOT="${APP_ROOT:-}"
16
16
  PM2_BIN="${PM2_BIN:-pm2}"
17
17
  # 实例数(单实例 fork;SQLite 单文件写串行,不建议 cluster 多实例)
18
18
  INSTANCES="${INSTANCES:-1}"
19
+ # 版本轮转保留数(releases 目录数,即回滚窗口;顶层 install.sh::rotate_releases 读)
20
+ KEEP_VERSIONS="${KEEP_VERSIONS:-3}"
19
21
 
20
22
  # ── 派生路径(APP_ROOT 定下来后才能算;由 resolve_app_root 调用) ──
21
23
  derive_paths() {
@@ -16,6 +16,8 @@ APP_ROOT="${APP_ROOT:-}"
16
16
  NODE_BIN="${NODE_BIN:-/usr/bin/node}"
17
17
  # 运行用户
18
18
  RUN_USER="${RUN_USER:-root}"
19
+ # 版本轮转保留数(releases 目录数,即回滚窗口;顶层 install.sh::rotate_releases 读)
20
+ KEEP_VERSIONS="${KEEP_VERSIONS:-3}"
19
21
 
20
22
  # ── 派生路径(APP_ROOT 定下来后才能算;由 resolve_app_root 调用) ──
21
23
  derive_paths() {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend",
3
- "version": "1.0.2",
3
+ "version": "1.0.7",
4
4
  "description": "全栈模板 - 后台服务(NestJS + SQLite + TypeORM)",
5
5
  "private": true,
6
6
  "engines": {
@@ -20,7 +20,6 @@
20
20
  "migration:generate": "TS_NODE_PROJECT=tsconfig.migration.json typeorm-ts-node-commonjs migration:generate --pretty -d scripts/data-source.ts"
21
21
  },
22
22
  "dependencies": {
23
- "@nestjs/axios": "4.0.1",
24
23
  "@nestjs/common": "11.1.28",
25
24
  "@nestjs/config": "4.0.4",
26
25
  "@nestjs/core": "11.1.28",
@@ -37,16 +36,16 @@
37
36
  "better-sqlite3": "13.0.3",
38
37
  "class-transformer": "0.5.1",
39
38
  "class-validator": "0.15.1",
40
- "js-yaml": "4.1.0",
41
- "multer": "1.4.5-lts.1",
39
+ "js-yaml": "4.3.2",
40
+ "multer": "2.4.0",
42
41
  "passport": "0.7.0",
43
42
  "passport-jwt": "4.0.1",
44
43
  "passport-local": "1.0.0",
45
44
  "reflect-metadata": "0.2.2",
46
45
  "rxjs": "7.8.2",
47
- "socket.io": "4.8.3",
46
+ "socket.io": "4.8.4",
48
47
  "svg-captcha": "1.4.0",
49
- "typeorm": "0.3.20"
48
+ "typeorm": "0.3.31"
50
49
  },
51
50
  "devDependencies": {
52
51
  "@nestjs/cli": "11.0.24",
@@ -69,5 +68,13 @@
69
68
  "ts-node": "10.9.2",
70
69
  "tsconfig-paths": "4.2.0",
71
70
  "typescript": "5.4.3"
71
+ },
72
+ "overrides": {
73
+ "proxy-addr": "^2.0.8",
74
+ "multer": "$multer",
75
+ "engine.io": "^6.6.10",
76
+ "@nestjs/swagger": {
77
+ "js-yaml": "^5.4.3"
78
+ }
72
79
  }
73
80
  }
@@ -32,6 +32,7 @@
32
32
  */
33
33
  const fs = require('fs');
34
34
  const path = require('path');
35
+ const os = require('os');
35
36
  const readline = require('readline');
36
37
 
37
38
  const BACKEND = path.resolve(__dirname, '..');
@@ -265,6 +266,70 @@ function cleanAppleDoubles(dir) {
265
266
  return removed;
266
267
  }
267
268
 
269
+ // 生成/刷新 backend/package-lock.json(Dockerfile `npm ci` 用的真 npm lockfile)。
270
+ // 生成必须在隔离目录(一次性 mkdtemp),直接在 backend/ 下跑 npm 会踩两个雷(实测):
271
+ // ① 根 package.json 声明 workspaces → npm 自动爬到 workspace 根解析整个 monorepo,
272
+ // 撞上 pnpm 符号链 node_modules → 崩溃/hang(Cannot read properties of null / 长时间无输出);
273
+ // ② 加 --workspaces=false 钉住单包后,npm 又会读 backend/node_modules 的 pnpm 符号链
274
+ // 实际树,把 resolved 写成 "../node_modules/.pnpm/..." 伪路径(即 validateLockfile
275
+ // 防的污染,进 Docker npm ci 建出断链 node_modules)。
276
+ // 隔离目录无父 workspace、无 node_modules → npm 只能按 registry 解析 → 真 lockfile,
277
+ // 产物拷回 backend/package-lock.json(落位与 README「第 4 步」手工配方一致)。触发条件:
278
+ // ① lock 不存在 → 生成(免手工);
279
+ // ② package.json 的 dependencies 与 lock packages[""].dependencies 不同步 → 刷新
280
+ // (pnpm install 改依赖后自动跟上,防 Dockerfile 按旧 lock 装出旧树;syncLockVersion
281
+ // 只对 version 字段管不了依赖树)。devDependencies 不比对:lock 以 --omit=dev 生成,
282
+ // 不含 dev 树,devDeps 变更不影响容器安装。
283
+ // 失败 throw 附手工兜底命令(与 README「第 4 步」同款)。
284
+ function sameDeps(a, b) {
285
+ const ka = Object.keys(a).sort();
286
+ const kb = Object.keys(b).sort();
287
+ return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
288
+ }
289
+
290
+ function lockDepsOutOfSync(lockPath, pkg) {
291
+ try {
292
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
293
+ const lockDeps = (lock.packages && lock.packages[''] && lock.packages[''].dependencies) || {};
294
+ return !sameDeps(lockDeps, pkg.dependencies || {});
295
+ } catch {
296
+ return true; // lock 损坏/不可解析 → 视为不同步,重生成
297
+ }
298
+ }
299
+
300
+ function ensureLockfile() {
301
+ const lockPath = path.join(BACKEND, 'package-lock.json');
302
+ const missing = !fs.existsSync(lockPath);
303
+ if (!missing && !lockDepsOutOfSync(lockPath, readPkg())) return;
304
+ console.log(
305
+ missing
306
+ ? '▶ backend/package-lock.json 不存在(Dockerfile npm ci 依赖它),自动生成 ...'
307
+ : '▶ package.json 依赖与 lock 不同步,自动刷新 backend/package-lock.json ...'
308
+ );
309
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'be-lock-'));
310
+ try {
311
+ fs.copyFileSync(PKG_PATH, path.join(tmp, 'package.json'));
312
+ const r = require('child_process').spawnSync(
313
+ 'npm',
314
+ ['install', '--package-lock-only', '--omit=dev', '--legacy-peer-deps'],
315
+ { cwd: tmp, stdio: 'inherit', shell: true }
316
+ );
317
+ const tmpLock = path.join(tmp, 'package-lock.json');
318
+ if (r.error || r.status !== 0 || !fs.existsSync(tmpLock)) {
319
+ throw new Error(
320
+ `npm install --package-lock-only 生成 lockfile 失败(status=${r.status})。手工兜底:\n` +
321
+ ' mkdir -p /tmp/be-lock && cp backend/package.json /tmp/be-lock/ \\\n' +
322
+ ' && (cd /tmp/be-lock && npm install --package-lock-only --omit=dev --legacy-peer-deps \\\n' +
323
+ ' && cp package-lock.json <repo>/backend/)'
324
+ );
325
+ }
326
+ fs.copyFileSync(tmpLock, lockPath);
327
+ } finally {
328
+ fs.rmSync(tmp, { recursive: true, force: true });
329
+ }
330
+ console.log(' ✓ backend/package-lock.json 已生成/刷新');
331
+ }
332
+
268
333
  // 防御:package-lock.json 必须是真 npm lockfile(resolved = registry URL)。
269
334
  // pnpm workspace 下误跑 `npm install` 会把现有 pnpm 符号链当 resolved 路径写进 lock
270
335
  // (resolved = "../node_modules/.pnpm/..."),Dockerfile `npm ci` 据此建出指向容器内不存在的
@@ -272,7 +337,7 @@ function cleanAppleDoubles(dir) {
272
337
  // buildops 时拦此污染,提示重生路径。
273
338
  function validateLockfile() {
274
339
  const lockPath = path.join(BACKEND, 'package-lock.json');
275
- if (!fs.existsSync(lockPath)) return; // 未生成时跳过(Dockerfile npm ci 另因缺 lock 报错)
340
+ if (!fs.existsSync(lockPath)) return; // 正常已被上方 ensureLockfile 生成,此处纯防御
276
341
  const raw = fs.readFileSync(lockPath, 'utf8');
277
342
  if (raw.includes('node_modules/.pnpm')) {
278
343
  throw new Error(
@@ -287,6 +352,7 @@ function validateLockfile() {
287
352
 
288
353
  // 组装运维包
289
354
  function buildOps(version) {
355
+ ensureLockfile();
290
356
  validateLockfile();
291
357
  const pkg = readPkg();
292
358
  // name 路径安全化:backend → backend
@@ -48,6 +48,15 @@ export interface AppConfig {
48
48
  username: string;
49
49
  password: string;
50
50
  };
51
+ /** 单点登录 /api/ssologin(演示接入范式,默认关闭) */
52
+ sso: {
53
+ /** 是否开启;false 时接口返回 404(对外不暴露该能力) */
54
+ enabled: boolean;
55
+ /** 票据校验密钥(HMAC-SHA256),与接入方(门户/统一认证)共享 */
56
+ ticketKey: string;
57
+ /** 时间戳允许偏差(秒),默认 300 */
58
+ tsWindow: number;
59
+ };
51
60
  /** 大屏 API 签名加密配置(无 token、无过期) */
52
61
  appSign: {
53
62
  /** HMAC-SHA256 签名密钥,永不过期 */
@@ -27,6 +27,11 @@ const envInt = (k: string, fallback?: number): number | undefined => {
27
27
  const n = Number(v);
28
28
  return Number.isFinite(n) ? n : fallback;
29
29
  };
30
+ const envBool = (k: string, fallback?: boolean): boolean | undefined => {
31
+ const v = process.env[k];
32
+ if (v === undefined || v === '') return fallback;
33
+ return v === 'true' || v === '1';
34
+ };
30
35
  const envList = (k: string, fallback?: string[]): string[] | undefined => {
31
36
  const v = process.env[k];
32
37
  if (v === undefined || v === '') return fallback;
@@ -102,6 +107,12 @@ export default (): AppConfig => {
102
107
  ...config.appSign,
103
108
  signKey: envStr('APP_SIGN_KEY', config.appSign.signKey) as string,
104
109
  tsWindow: envInt('APP_SIGN_TS_WINDOW', config.appSign.tsWindow) as number
110
+ },
111
+ sso: {
112
+ ...config.sso,
113
+ enabled: envBool('SSO_ENABLED', config.sso.enabled) as boolean,
114
+ ticketKey: envStr('SSO_TICKET_KEY', config.sso.ticketKey) as string,
115
+ tsWindow: envInt('SSO_TS_WINDOW', config.sso.tsWindow) as number
105
116
  }
106
117
  };
107
118
  };
@@ -1,16 +1,20 @@
1
- import { Injectable, UnauthorizedException } from '@nestjs/common';
1
+ import { Injectable, UnauthorizedException, NotFoundException, Logger } from '@nestjs/common';
2
2
  import { JwtService } from '@nestjs/jwt';
3
3
  import { ConfigService } from '@nestjs/config';
4
4
  import { InjectRepository } from '@nestjs/typeorm';
5
5
  import { Repository } from 'typeorm';
6
6
  import * as bcrypt from 'bcryptjs';
7
+ import * as crypto from 'crypto';
7
8
  import { UserEntity } from '../../entities/user.entity';
8
9
  import { CaptchaService } from './captcha.service';
9
10
  import { AuditService } from '../audit/audit.service';
11
+ import { safeEqualHex } from '../../common/app-crypto';
10
12
  import type { AppConfig } from '../../config/config.interface';
11
13
 
12
14
  @Injectable()
13
15
  export class AuthService {
16
+ private readonly logger = new Logger(AuthService.name);
17
+
14
18
  constructor(
15
19
  @InjectRepository(UserEntity)
16
20
  private readonly userRepo: Repository<UserEntity>,
@@ -56,10 +60,14 @@ export class AuthService {
56
60
  });
57
61
  throw new UnauthorizedException('用户名或密码错误');
58
62
  }
59
- const ok = user.password.startsWith('$2')
60
- ? await bcrypt.compare(params.password, user.password)
61
- : params.password === user.password;
63
+ // 非 bcrypt 哈希(历史明文/手工改库)一律拒绝,不做明文比对回退;
64
+ // 存量明文用户走 /api/user/resetPassword 恢复,超管走「改配置+重启」reconcile。
65
+ const hashed = user.password.startsWith('$2');
66
+ const ok = hashed && (await bcrypt.compare(params.password, user.password));
62
67
  if (!ok) {
68
+ if (!hashed) {
69
+ this.logger.warn(`用户 ${user.username} 密码非 bcrypt 哈希,请管理员重置`);
70
+ }
63
71
  await this.auditService.record({
64
72
  actorName: params.username,
65
73
  action: 'login_fail',
@@ -88,23 +96,60 @@ export class AuthService {
88
96
  }
89
97
 
90
98
  /**
91
- * 单点登录 /api/ssologin
99
+ * 单点登录 /api/ssologin(演示接入范式,默认关闭)
100
+ *
101
+ * 未开启(yaml sso.enabled=false)时返回 404,对外不暴露该能力。
102
+ * 开启后校验接入方票据:
103
+ * GET /api/ssologin?username=xxx&ts=<unix秒>&sign=<hex>
104
+ * sign = HMAC-SHA256(`${username}\n${ts}`, sso.ticketKey) hex
105
+ * 接入方(门户/统一认证)持 ticketKey 生成跳转 URL,票据时效由 tsWindow 约束。
106
+ * role 固定 'web'(最小权限),勿给该角色挂敏感路由。
92
107
  */
93
- async ssoLogin(params: { [key: string]: any }, ip?: string) {
94
- if (!params || Object.keys(params).length === 0) {
108
+ async ssoLogin(
109
+ params: { username?: string; ts?: string; sign?: string },
110
+ ip?: string
111
+ ) {
112
+ const ssoCfg = this.config.get<AppConfig['sso']>('sso');
113
+ const username = params?.username || '';
114
+
115
+ if (!ssoCfg.enabled) {
116
+ throw new NotFoundException('接口不存在');
117
+ }
118
+
119
+ const fail = async (reason: string) => {
95
120
  await this.auditService.record({
96
- actorName: params?.username || 'sso',
121
+ actorName: username || 'sso',
97
122
  action: 'login_fail',
98
- detail: { reason: '缺少 sso 参数' },
123
+ detail: { reason },
99
124
  ip
100
125
  });
101
- throw new UnauthorizedException('缺少 sso 参数');
126
+ throw new UnauthorizedException(reason);
127
+ };
128
+
129
+ const tsNum = Number(params.ts);
130
+ const nowSec = Math.floor(Date.now() / 1000);
131
+ if (!username || !params.ts || !params.sign) {
132
+ await fail('缺少单点登录票据参数');
133
+ }
134
+ if (!Number.isFinite(tsNum) || tsNum <= 0) {
135
+ await fail('票据时间戳非法');
136
+ }
137
+ if (Math.abs(nowSec - tsNum) > ssoCfg.tsWindow) {
138
+ await fail('票据已过期');
102
139
  }
103
- const username = params.username || 'sso';
140
+
141
+ const expected = crypto
142
+ .createHmac('sha256', ssoCfg.ticketKey)
143
+ .update(`${username}\n${params.ts}`, 'utf8')
144
+ .digest('hex');
145
+ if (!safeEqualHex(expected, String(params.sign).toLowerCase())) {
146
+ await fail('票据校验失败');
147
+ }
148
+
104
149
  await this.auditService.record({
105
150
  actorName: username,
106
151
  action: 'login_success',
107
- detail: { sso: true, params: Object.keys(params) },
152
+ detail: { sso: true },
108
153
  ip
109
154
  });
110
155
  const payload = { sub: 0, username, role: 'web' };
@@ -8,49 +8,72 @@ import {
8
8
  Query,
9
9
  Request,
10
10
  UploadedFile,
11
- UseInterceptors
11
+ UseGuards,
12
+ UseInterceptors,
13
+ ForbiddenException
12
14
  } from '@nestjs/common';
13
15
  import { FileInterceptor } from '@nestjs/platform-express';
14
- import { ApiTags, ApiConsumes, ApiBody } from '@nestjs/swagger';
16
+ import { ApiTags, ApiConsumes, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
15
17
  import { PageDataService } from './page-data.service';
18
+ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
16
19
 
17
20
  @ApiTags('page-data 大屏页面数据')
18
21
  @Controller()
19
22
  export class PageDataController {
20
23
  constructor(private readonly service: PageDataService) {}
21
24
 
22
- /** 列表 /api/getList */
25
+ /** 仅管理员可写(与 user.controller 的 ensureAdmin 同模式) */
26
+ private ensureAdmin(req: any) {
27
+ if (req.user?.role !== 'admin' && req.user?.role !== 'superadmin') {
28
+ throw new ForbiddenException('仅管理员可操作大屏页面数据');
29
+ }
30
+ return { id: req.user.id, username: req.user.username };
31
+ }
32
+
33
+ /**
34
+ * 列表 /api/getList
35
+ * 保持公开:docs/api-security.md §1.1/§6 已拍板「暂不纳入签名保护」(大屏消费接口)。
36
+ */
23
37
  @Get('getList')
24
38
  getList(@Query() query: any) {
25
39
  return this.service.getList(query);
26
40
  }
27
41
 
28
- /** 新增 /api/add */
42
+ /** 新增 /api/add(需登录,admin-only) */
43
+ @UseGuards(JwtAuthGuard)
29
44
  @Post('add')
30
45
  add(@Body() body: any, @Request() req: any) {
46
+ this.ensureAdmin(req);
31
47
  return this.service.add(body, req.user);
32
48
  }
33
49
 
34
- /** 修改 /api/edit */
50
+ /** 修改 /api/edit(需登录,admin-only) */
51
+ @UseGuards(JwtAuthGuard)
35
52
  @Put('edit')
36
- edit(@Body() body: any) {
53
+ edit(@Body() body: any, @Request() req: any) {
54
+ this.ensureAdmin(req);
37
55
  return this.service.update(body);
38
56
  }
39
57
 
40
- /** 删除 /api/deleteById */
58
+ /** 删除 /api/deleteById(需登录,admin-only) */
59
+ @UseGuards(JwtAuthGuard)
41
60
  @Delete('deleteById')
42
- deleteById(@Query('id') id: string) {
61
+ deleteById(@Query('id') id: string, @Request() req: any) {
62
+ this.ensureAdmin(req);
43
63
  return this.service.deleteById(Number(id));
44
64
  }
45
65
 
46
- /** 导入文件 /api/v1/import */
66
+ /** 导入文件 /api/v1/import(需登录,admin-only) */
67
+ @UseGuards(JwtAuthGuard)
47
68
  @Post('v1/import')
48
69
  @UseInterceptors(FileInterceptor('file'))
49
70
  @ApiConsumes('multipart/form-data')
71
+ @ApiBearerAuth()
50
72
  @ApiBody({
51
73
  schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' } } }
52
74
  })
53
- import(@UploadedFile() file: any) {
75
+ import(@UploadedFile() file: any, @Request() req: any) {
76
+ this.ensureAdmin(req);
54
77
  if (!file) {
55
78
  throw new Error('缺少 file');
56
79
  }
@@ -1,22 +1,50 @@
1
- import { Controller, Get, Param, Body, Put } from '@nestjs/common';
2
- import { ApiTags } from '@nestjs/swagger';
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Param,
5
+ Body,
6
+ Put,
7
+ Request,
8
+ UseGuards,
9
+ ForbiddenException
10
+ } from '@nestjs/common';
11
+ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
3
12
  import { ScreenConfigService } from './screen-config.service';
4
13
  import { ScreenConfigEntity } from '../../entities/screen-config.entity';
14
+ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
5
15
 
6
16
  @ApiTags('screen 大屏配置')
7
17
  @Controller('screen')
8
18
  export class ScreenConfigController {
9
19
  constructor(private readonly service: ScreenConfigService) {}
10
20
 
11
- /** 大屏清单 /api/screen/list */
21
+ /** 仅管理员可写(与 user.controller 的 ensureAdmin 同模式) */
22
+ private ensureAdmin(req: any) {
23
+ if (req.user?.role !== 'admin' && req.user?.role !== 'superadmin') {
24
+ throw new ForbiddenException('仅管理员可操作大屏配置');
25
+ }
26
+ return { id: req.user.id, username: req.user.username };
27
+ }
28
+
29
+ /**
30
+ * 大屏清单 /api/screen/list
31
+ * 保持公开:docs/api-security.md §1.1/§6 已拍板「暂不纳入签名保护」(大屏消费接口)。
32
+ */
12
33
  @Get('list')
13
34
  list() {
14
35
  return this.service.list();
15
36
  }
16
37
 
17
- /** 更新大屏配置 /api/screen/:key */
38
+ /** 更新大屏配置 /api/screen/:key(需登录,admin-only) */
39
+ @UseGuards(JwtAuthGuard)
40
+ @ApiBearerAuth()
18
41
  @Put(':key')
19
- update(@Param('key') key: string, @Body() dto: Partial<ScreenConfigEntity>) {
42
+ update(
43
+ @Param('key') key: string,
44
+ @Body() dto: Partial<ScreenConfigEntity>,
45
+ @Request() req: any
46
+ ) {
47
+ this.ensureAdmin(req);
20
48
  return this.service.update(key, dto);
21
49
  }
22
50
  }
@@ -50,7 +50,7 @@ scripts/ 根级发布/回滚薄编排
50
50
  7. **两个请求实例禁止复用**:`request.ts`(后台,带 Authorization)与 `app-request.ts`(大屏,三头签名)独立,复用会互相头污染。
51
51
  8. **DB 文件名程序定死**:`template.prod.sqlite`/`template.dev.sqlite`,改名要同步 `sqlite.config.ts`、`configuration.ts`、`ops/sqlite/config.sh`、ops 探针四处。
52
52
  9. **better-sqlite3 v13 无预编译二进制**:需 python3/make/g++ 源码编译(Dockerfile 已装齐);本地装依赖报 node-gyp 错先查这个。
53
- 10. **backend/package-lock.json 必须是真 npm lockfile**(Dockerfile `npm ci` 用;首次部署前按 README「第 4 步」重生,避开 pnpm 符号链污染)。
53
+ 10. **backend/package-lock.json 必须是真 npm lockfile**(Dockerfile `npm ci` 用;buildops 自动生成/刷新——缺失或依赖不同步时在隔离目录跑 `npm install --package-lock-only` 拷回,无需手工;在 `backend/` 下裸跑 npm 会撞 workspace 根探测/pnpm 符号链污染,详见 `build.cjs::ensureLockfile` 注释与 README「第 4 步」)。
54
54
 
55
55
  ## 编码与接口约定
56
56