@gadmin2n/schematics 0.0.157 → 0.0.159
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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/patches/@nestjs__cli@11.0.21.patch +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/pnpm-workspace.yaml +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-swc-watch-leak.js +309 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.test.js +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-export.decorator.spec.ts +34 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-export.decorator.ts +42 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/require-user-group.decorator.spec.ts +23 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/require-user-group.decorator.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.guard.spec.ts +90 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.guard.ts +51 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/userGroup.module.ts +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/vite.config.ts +1 -1
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"start:debug": "dotenv -e .env.local -e .env -- nest start -c gadmin-cli.json --debug --watch",
|
|
19
19
|
"start:prod": "/bin/sh start-prod.sh",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"check:swc-leak": "node scripts/check-swc-watch-leak.js",
|
|
21
22
|
"lint": "oxlint src test",
|
|
22
23
|
"lint:fix": "oxlint src test --fix",
|
|
23
24
|
"test": "jest",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"migrate:deploy": "dotenv -e .env.local -e .env -- prisma migrate deploy",
|
|
31
32
|
"migrate:status": "dotenv -e .env.local -e .env -- prisma migrate status",
|
|
32
33
|
"generate": "dotenv -e .env.local -e .env -- npx prisma generate",
|
|
33
|
-
"generate:dev": "
|
|
34
|
+
"generate:dev": "npm run migrate:dev",
|
|
34
35
|
"postgenerate:dev": "dotenv -e .env.local -e .env -- ts-node seed/data-mngt.seed.ts",
|
|
35
36
|
"postgadminGenerate": "node scripts/notify-dev-servers.js",
|
|
36
37
|
"push:db": "dotenv -e .env.local -e .env -- prisma db push --skip-generate",
|
|
@@ -151,7 +152,7 @@
|
|
|
151
152
|
"node": ">=22.13"
|
|
152
153
|
},
|
|
153
154
|
"prisma": {
|
|
154
|
-
"seed": "
|
|
155
|
+
"seed": "npm run seed"
|
|
155
156
|
},
|
|
156
157
|
"lint-staged": {
|
|
157
158
|
"**/*.{ts,tsx}": "oxfmt"
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/patches/@nestjs__cli@11.0.21.patch
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
diff --git a/lib/compiler/swc/swc-compiler.js b/lib/compiler/swc/swc-compiler.js
|
|
2
|
+
index 5e8fdad81ea71216755876461c3835a23df1c653..ff2441021b5f294fa859b1553924cc558f69de02 100644
|
|
3
|
+
--- a/lib/compiler/swc/swc-compiler.js
|
|
4
|
+
+++ b/lib/compiler/swc/swc-compiler.js
|
|
5
|
+
@@ -126,6 +126,15 @@ class SwcCompiler extends base_compiler_1.BaseCompiler {
|
|
6
|
+
cliOptions: {
|
|
7
|
+
...swcCliOpts.cliOptions,
|
|
8
|
+
filenames: [file],
|
|
9
|
+
+ // PATCH: this call only transpiles the single newly-added file.
|
|
10
|
+
+ // Leaving watch=true made swcDir() run watchCompilation() again,
|
|
11
|
+
+ // leaking a chokidar watcher per event; and sync=false made it
|
|
12
|
+
+ // spawn a Piscina pool (never destroyed upstream) just for one
|
|
13
|
+
+ // file. sync=true takes the direct dirWorker path: no pool, no
|
|
14
|
+
+ // watcher, and ~40x faster for a single file.
|
|
15
|
+
+ // Upstream: https://github.com/swc-project/pkgs/issues/126
|
|
16
|
+
+ watch: false,
|
|
17
|
+
+ sync: true,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-swc-watch-leak.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @nestjs/cli swc watch 模式的线程/句柄泄漏回归检测。
|
|
4
|
+
*
|
|
5
|
+
* node scripts/check-swc-watch-leak.js
|
|
6
|
+
*
|
|
7
|
+
* 背景:
|
|
8
|
+
* nest CLI 的 swc builder 在 watch 模式下,每个「新增文件」事件都会重新调用
|
|
9
|
+
* @swc/cli 的 swcDir(),且传入的 cliOptions 仍带 watch:true / sync:false:
|
|
10
|
+
*
|
|
11
|
+
* - sync:false → swcDir 走 Piscina 分支,为编译 1 个文件创建一个 worker
|
|
12
|
+
* pool;而上游 @swc/cli 从不 destroy 它(issue 见下),
|
|
13
|
+
* 于是每次事件永久钉住 cores/2 个 idle worker。
|
|
14
|
+
* - watch:true → swcDir 末尾又跑一次 watchCompilation(),每次事件多泄漏
|
|
15
|
+
* 一个 chokidar watcher(各带 fd/kqueue)。
|
|
16
|
+
*
|
|
17
|
+
* 实测(14 核 macOS):每次新增文件 +7 常驻线程,8 次后 RSS 从 90MB 涨到
|
|
18
|
+
* 681MB;长时间 dev 会话可累积上千线程、十余 GB 足迹。
|
|
19
|
+
*
|
|
20
|
+
* 上游 issue:https://github.com/swc-project/pkgs/issues/126
|
|
21
|
+
* (@swc/cli 0.8.1 与 @nestjs/cli 11.0.24 均未修复)
|
|
22
|
+
*
|
|
23
|
+
* 本项目的处置:
|
|
24
|
+
* patches/@nestjs__cli@<version>.patch 给 file-add 回调补上
|
|
25
|
+
* watch:false + sync:true —— 单文件改走 dirWorker 直连,既不建 pool 也不建
|
|
26
|
+
* watcher,且比原路径快约 40 倍。initial 全量编译不受影响(仍是 sync:false
|
|
27
|
+
* 并行),因为 nest 那里是 spread 出的独立对象。
|
|
28
|
+
*
|
|
29
|
+
* 何时需要跑本脚本:
|
|
30
|
+
* 升级 @nestjs/cli 或 @swc/cli 之后。patch 锚点是编译产物,上游重构可能让它
|
|
31
|
+
* 失效;pnpm 在 patch 无法应用时会直接报错,但「能应用却语义变了」只能靠这里
|
|
32
|
+
* 兜住。
|
|
33
|
+
*
|
|
34
|
+
* 退出码:0 = 正常,1 = 检测到回归。
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const os = require('os');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
const { execSync } = require('child_process');
|
|
41
|
+
|
|
42
|
+
const SERVER_DIR = path.join(__dirname, '..');
|
|
43
|
+
const SWC_COMPILER = path.join(
|
|
44
|
+
SERVER_DIR,
|
|
45
|
+
'node_modules/@nestjs/cli/lib/compiler/swc/swc-compiler.js',
|
|
46
|
+
);
|
|
47
|
+
const SWC_DIR_MOD = path.join(SERVER_DIR, 'node_modules/@swc/cli/lib/swc/dir.js');
|
|
48
|
+
|
|
49
|
+
const RED = '\x1b[31m';
|
|
50
|
+
const GREEN = '\x1b[32m';
|
|
51
|
+
const YELLOW = '\x1b[33m';
|
|
52
|
+
const DIM = '\x1b[90m';
|
|
53
|
+
const RESET = '\x1b[0m';
|
|
54
|
+
|
|
55
|
+
const problems = [];
|
|
56
|
+
|
|
57
|
+
function ok(msg) {
|
|
58
|
+
console.log(`${GREEN}✓${RESET} ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
function bad(msg) {
|
|
61
|
+
console.log(`${RED}✗${RESET} ${msg}`);
|
|
62
|
+
problems.push(msg);
|
|
63
|
+
}
|
|
64
|
+
function info(msg) {
|
|
65
|
+
console.log(`${DIM} ${msg}${RESET}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 本进程线程数(不含主线程记账差异,仅用于相对比较)。 */
|
|
69
|
+
function threadCount() {
|
|
70
|
+
if (process.platform === 'linux') {
|
|
71
|
+
return fs.readdirSync('/proc/self/task').length;
|
|
72
|
+
}
|
|
73
|
+
// darwin:ps -M 每行一个线程,首行是表头
|
|
74
|
+
const out = execSync(`ps -M ${process.pid}`, { encoding: 'utf8' });
|
|
75
|
+
return out.trim().split('\n').length - 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleCount() {
|
|
79
|
+
return typeof process._getActiveHandles === 'function'
|
|
80
|
+
? process._getActiveHandles().length
|
|
81
|
+
: -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------- 静态检查:patch 是否还在,且语义是我们要的 ----------
|
|
85
|
+
|
|
86
|
+
function checkPatchApplied() {
|
|
87
|
+
if (!fs.existsSync(SWC_COMPILER)) {
|
|
88
|
+
bad(`找不到 ${path.relative(SERVER_DIR, SWC_COMPILER)},@nestjs/cli 未安装?`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const src = fs.readFileSync(SWC_COMPILER, 'utf8');
|
|
92
|
+
|
|
93
|
+
// 锚点:nest 自己的注释。上游若重构掉它,说明 patch 需要重新审查。
|
|
94
|
+
const anchor = 'Transpile newly added file';
|
|
95
|
+
const at = src.indexOf(anchor);
|
|
96
|
+
if (at === -1) {
|
|
97
|
+
bad(
|
|
98
|
+
`swc-compiler.js 里找不到锚点注释 "${anchor}"——上游结构已变,` +
|
|
99
|
+
`patch 可能已失效,需重新审查 patches/ 下的补丁`,
|
|
100
|
+
);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 精确截出锚点之后那一次 swcCli.default(...) 的实参范围:
|
|
105
|
+
// 用括号配平,避免固定窗口切断(patch 注释较长)或越界到相邻函数。
|
|
106
|
+
const callAt = src.indexOf('swcCli.default(', at);
|
|
107
|
+
if (callAt === -1) {
|
|
108
|
+
bad('锚点之后找不到 swcCli.default(...) 调用——上游结构已变,需重新审查 patch');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
let depth = 0;
|
|
112
|
+
let end = -1;
|
|
113
|
+
for (let i = src.indexOf('(', callAt); i < src.length; i++) {
|
|
114
|
+
if (src[i] === '(') depth += 1;
|
|
115
|
+
else if (src[i] === ')') {
|
|
116
|
+
depth -= 1;
|
|
117
|
+
if (depth === 0) {
|
|
118
|
+
end = i;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (end === -1) {
|
|
124
|
+
bad('swcCli.default(...) 括号无法配平,跳过静态检查');
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const region = src.slice(callAt, end + 1);
|
|
128
|
+
// 合理性校验:这段必须是那个 cliOptions 对象,否则说明定位错了
|
|
129
|
+
if (!region.includes('filenames')) {
|
|
130
|
+
bad('定位到的 swcCli.default(...) 实参不含 filenames,静态检查定位失败');
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const hasWatchFalse = /watch:\s*false/.test(region);
|
|
134
|
+
const hasSyncTrue = /sync:\s*true/.test(region);
|
|
135
|
+
|
|
136
|
+
if (hasWatchFalse && hasSyncTrue) {
|
|
137
|
+
ok('patch 已生效:file-add 回调带 watch:false + sync:true');
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
bad(
|
|
141
|
+
'patch 未生效:file-add 回调缺少 watch:false / sync:true' +
|
|
142
|
+
`(watch:false=${hasWatchFalse}, sync:true=${hasSyncTrue})。` +
|
|
143
|
+
'执行 pnpm install 重新应用,或按新版本重打 patch',
|
|
144
|
+
);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 顺带提示 @swc/cli 上游是否已自行修复(届时可考虑撤掉本项目的 patch)。 */
|
|
149
|
+
function checkUpstreamSwcCli() {
|
|
150
|
+
if (!fs.existsSync(SWC_DIR_MOD)) return;
|
|
151
|
+
const src = fs.readFileSync(SWC_DIR_MOD, 'utf8');
|
|
152
|
+
const hasPool = /new piscina_1\.default\(/.test(src);
|
|
153
|
+
const hasDestroy = /workers\.destroy\(\)/.test(src);
|
|
154
|
+
if (hasPool && hasDestroy) {
|
|
155
|
+
console.log(
|
|
156
|
+
`${YELLOW}!${RESET} @swc/cli 现在会 destroy worker pool(上游似已修复)——` +
|
|
157
|
+
'可复核是否还需要本项目的 nest patch',
|
|
158
|
+
);
|
|
159
|
+
} else if (hasPool) {
|
|
160
|
+
info('@swc/cli 仍未 destroy worker pool(上游未修复,patch 仍有必要)');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- 动态检查:按 nest 的调用形状实跑,量泄漏 ----------
|
|
165
|
+
|
|
166
|
+
const swcOptions = {
|
|
167
|
+
sourceMaps: true,
|
|
168
|
+
module: { type: 'commonjs' },
|
|
169
|
+
jsc: {
|
|
170
|
+
target: 'es2020',
|
|
171
|
+
parser: { syntax: 'typescript', decorators: true, dynamicImport: true },
|
|
172
|
+
transform: { legacyDecorator: true, decoratorMetadata: true },
|
|
173
|
+
keepClassNames: true,
|
|
174
|
+
},
|
|
175
|
+
minify: false,
|
|
176
|
+
swcrc: false,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async function measure(swcDir, label, shape, rounds, tmpRoot) {
|
|
180
|
+
const src = path.join(tmpRoot, `${label}-src`);
|
|
181
|
+
const out = path.join(tmpRoot, `${label}-dist`);
|
|
182
|
+
fs.mkdirSync(src, { recursive: true });
|
|
183
|
+
|
|
184
|
+
const before = { t: threadCount(), h: handleCount() };
|
|
185
|
+
const times = [];
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < rounds; i++) {
|
|
188
|
+
const file = path.join(src, `probe${i}.ts`);
|
|
189
|
+
fs.writeFileSync(
|
|
190
|
+
file,
|
|
191
|
+
"import { Injectable } from '@nestjs/common';\n" +
|
|
192
|
+
`@Injectable()\nexport class Probe${i} {\n` +
|
|
193
|
+
` private readonly n = ${i};\n` +
|
|
194
|
+
' run(x: number): string { return `${this.n}:${x}`; }\n}\n',
|
|
195
|
+
);
|
|
196
|
+
const started = Date.now();
|
|
197
|
+
await swcDir({
|
|
198
|
+
cliOptions: {
|
|
199
|
+
outDir: out,
|
|
200
|
+
filenames: [path.relative(process.cwd(), file)],
|
|
201
|
+
extensions: ['.js', '.ts'],
|
|
202
|
+
copyFiles: false,
|
|
203
|
+
includeDotfiles: false,
|
|
204
|
+
quiet: true,
|
|
205
|
+
stripLeadingPaths: true,
|
|
206
|
+
...shape,
|
|
207
|
+
},
|
|
208
|
+
swcOptions,
|
|
209
|
+
});
|
|
210
|
+
times.push(Date.now() - started);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 给 piscina 的 eager spawn 留出时间,避免低估
|
|
214
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
215
|
+
|
|
216
|
+
const after = { t: threadCount(), h: handleCount() };
|
|
217
|
+
const compiled = fs.existsSync(out) ? fs.readdirSync(out).length : 0;
|
|
218
|
+
return {
|
|
219
|
+
dThreads: after.t - before.t,
|
|
220
|
+
dHandles: after.h - before.h,
|
|
221
|
+
avgMs: times.reduce((a, b) => a + b, 0) / times.length,
|
|
222
|
+
compiled,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
console.log('swc watch 泄漏回归检测\n');
|
|
228
|
+
|
|
229
|
+
const patchOk = checkPatchApplied();
|
|
230
|
+
checkUpstreamSwcCli();
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
if (!fs.existsSync(SWC_DIR_MOD)) {
|
|
234
|
+
bad('找不到 @swc/cli,跳过动态检测');
|
|
235
|
+
return finish();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const swcDir = require(SWC_DIR_MOD).default;
|
|
239
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swc-leak-'));
|
|
240
|
+
const ROUNDS = 6;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// 预热:@swc/core 的原生 binding 首次编译时会建自己的线程池,那是固有开销、
|
|
244
|
+
// 不是泄漏。用 sync:true 预热(该路径不创建 Piscina pool),免得记到第一组头上。
|
|
245
|
+
await measure(swcDir, 'warmup', { watch: false, sync: true }, 1, tmpRoot);
|
|
246
|
+
|
|
247
|
+
// 与 patch 后 nest 实际传参一致
|
|
248
|
+
const fixed = await measure(swcDir, 'fixed', { watch: false, sync: true }, ROUNDS, tmpRoot);
|
|
249
|
+
// 未打 patch 时 nest 的传参,作为对照,确认探针本身有区分力
|
|
250
|
+
const leaky = await measure(swcDir, 'leaky', { watch: true, sync: false }, ROUNDS, tmpRoot);
|
|
251
|
+
|
|
252
|
+
console.log(`${ROUNDS} 次 file-add(cpus=${os.cpus().length}):\n`);
|
|
253
|
+
console.log(' 调用形状 Δ线程 Δ句柄 平均耗时 产物');
|
|
254
|
+
console.log(' ' + '-'.repeat(62));
|
|
255
|
+
const row = (name, r) =>
|
|
256
|
+
` ${name.padEnd(28)} ${String(r.dThreads).padStart(5)} ${String(r.dHandles).padStart(6)}` +
|
|
257
|
+
` ${r.avgMs.toFixed(1).padStart(7)}ms ${r.compiled}`;
|
|
258
|
+
console.log(row('watch:false + sync:true', fixed));
|
|
259
|
+
console.log(row('watch:true + sync:false', leaky));
|
|
260
|
+
console.log('');
|
|
261
|
+
|
|
262
|
+
if (fixed.compiled === 0) {
|
|
263
|
+
bad('patch 形状下没有产物,编译链路异常');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (fixed.dThreads > 0 || fixed.dHandles > 0) {
|
|
267
|
+
bad(
|
|
268
|
+
`patch 形状仍在泄漏(Δ线程=${fixed.dThreads}, Δ句柄=${fixed.dHandles}),` +
|
|
269
|
+
'预期均为 0——@swc/cli 行为可能已改变',
|
|
270
|
+
);
|
|
271
|
+
} else {
|
|
272
|
+
ok('patch 形状零泄漏(Δ线程=0, Δ句柄=0)');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (leaky.dThreads <= 0 && leaky.dHandles <= 0) {
|
|
276
|
+
console.log(
|
|
277
|
+
`${YELLOW}!${RESET} 对照组也没泄漏——上游可能已修复,` +
|
|
278
|
+
'本项目的 patch 或许可以撤掉(请复核后再决定)',
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
info(
|
|
282
|
+
`对照组确认仍会泄漏(Δ线程=${leaky.dThreads}, Δ句柄=${leaky.dHandles}),` +
|
|
283
|
+
'探针有区分力',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!patchOk) {
|
|
291
|
+
info('静态检查未通过,即使动态指标正常也应复核 patch');
|
|
292
|
+
}
|
|
293
|
+
finish();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function finish() {
|
|
297
|
+
console.log('');
|
|
298
|
+
if (problems.length) {
|
|
299
|
+
console.log(`${RED}检测到 ${problems.length} 个问题${RESET}`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
console.log(`${GREEN}全部通过${RESET}`);
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
main().catch((e) => {
|
|
307
|
+
console.error(`${RED}探针异常${RESET}: ${e && e.stack ? e.stack : e}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* POSTGEN_POLL_MS 健康 poll 间隔,默认 250ms
|
|
28
28
|
* POSTGEN_INITIAL_STABLE_MS 初始稳定窗口,默认 3000ms
|
|
29
29
|
* POSTGEN_MAX_STABLE_MS 稳定窗口上限,默认 15000ms
|
|
30
|
-
* POSTGEN_OBSERVE_DOWN_TIMEOUT_MS 等待第一次 down 的超时,默认
|
|
30
|
+
* POSTGEN_OBSERVE_DOWN_TIMEOUT_MS 等待第一次 down 的超时,默认 5000ms
|
|
31
31
|
* POSTGEN_TOTAL_TIMEOUT_MS Phase 2 硬超时,默认 60000ms
|
|
32
32
|
*
|
|
33
33
|
* 与 CLI 通信约定:
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js
CHANGED
|
@@ -25,7 +25,9 @@ const DEFAULTS = {
|
|
|
25
25
|
initialStableMs: 3000,
|
|
26
26
|
maxStableMs: 15000,
|
|
27
27
|
observeDownTimeoutMs: 5000,
|
|
28
|
-
|
|
28
|
+
// 必须显著大于 maxStableMs:否则窗口一旦扩到上限,这里的硬超时总会先触发,
|
|
29
|
+
// 自适应窗口永远等不到"稳定"就被 timeout 放行(等于退化成固定等待)。
|
|
30
|
+
totalTimeoutMs: 60000,
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -58,7 +58,8 @@ test('nest 一开始就没跑:直接跳过', async () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test('Phase 1 超时(一直 up):跳过', async () => {
|
|
61
|
-
//
|
|
61
|
+
// 下面显式把 observeDownTimeoutMs 压到 1000ms,只需 1000/250 = 4 次 poll
|
|
62
|
+
// 就会命中 Phase 1 超时;100 次 up 是富余量。
|
|
62
63
|
const env = makeEnv(new Array(100).fill('up'));
|
|
63
64
|
const res = await waitForNestReady({
|
|
64
65
|
ping: env.ping,
|
|
@@ -18,6 +18,7 @@ import { ApiTags } from '@nestjs/swagger';
|
|
|
18
18
|
import { UseRoles } from 'nest-access-control';
|
|
19
19
|
import { PrismaService } from 'nestjs-prisma';
|
|
20
20
|
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
21
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
21
22
|
import { ImportPayloadDto } from './canvas-export.dto';
|
|
22
23
|
import { CanvasExportService } from './canvas-export.service';
|
|
23
24
|
import {
|
|
@@ -149,7 +150,7 @@ export class CanvasController {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
@Get('export/:code')
|
|
152
|
-
@
|
|
153
|
+
@AllowTransferExportRequest()
|
|
153
154
|
async getExportPayload(
|
|
154
155
|
@Param('code') code: string,
|
|
155
156
|
@Query('token') token: string,
|
|
@@ -46,9 +46,9 @@ import {
|
|
|
46
46
|
} from '../../generated/page/dto/page-types.dto';
|
|
47
47
|
import { UpdatePageDto } from '../../generated/page/dto/update-page.dto';
|
|
48
48
|
import { PageEntity } from '../../generated/page/entities/page.entity';
|
|
49
|
-
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
50
49
|
import { PageTransferService } from '../permTransfer/page-transfer.service';
|
|
51
50
|
import { ImportPayloadDto } from '../permTransfer/perm-transfer.dto';
|
|
51
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
52
52
|
import { PageService } from './page.service';
|
|
53
53
|
|
|
54
54
|
import { ConfigService } from '@nestjs/config';
|
|
@@ -248,7 +248,7 @@ export class PageController {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
@Get('export/:code')
|
|
251
|
-
@
|
|
251
|
+
@AllowTransferExportRequest()
|
|
252
252
|
async getExportPayload(
|
|
253
253
|
@Param('code') code: string,
|
|
254
254
|
@Query('token') token: string,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { setCredentialedExportCorsHeaders } from './transfer-export.decorator';
|
|
2
|
+
|
|
3
|
+
describe('setCredentialedExportCorsHeaders', () => {
|
|
4
|
+
it('reflects the request origin for credentialed export responses', () => {
|
|
5
|
+
const response = {
|
|
6
|
+
setHeader: jest.fn(),
|
|
7
|
+
vary: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
setCredentialedExportCorsHeaders('https://target.example.com', response);
|
|
11
|
+
|
|
12
|
+
expect(response.setHeader).toHaveBeenCalledWith(
|
|
13
|
+
'Access-Control-Allow-Origin',
|
|
14
|
+
'https://target.example.com',
|
|
15
|
+
);
|
|
16
|
+
expect(response.setHeader).toHaveBeenCalledWith(
|
|
17
|
+
'Access-Control-Allow-Credentials',
|
|
18
|
+
'true',
|
|
19
|
+
);
|
|
20
|
+
expect(response.vary).toHaveBeenCalledWith('Origin');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('leaves non-browser responses unchanged when Origin is absent', () => {
|
|
24
|
+
const response = {
|
|
25
|
+
setHeader: jest.fn(),
|
|
26
|
+
vary: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
setCredentialedExportCorsHeaders(undefined, response);
|
|
30
|
+
|
|
31
|
+
expect(response.setHeader).not.toHaveBeenCalled();
|
|
32
|
+
expect(response.vary).not.toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyDecorators,
|
|
3
|
+
NestInterceptor,
|
|
4
|
+
UseInterceptors,
|
|
5
|
+
} from '@nestjs/common';
|
|
6
|
+
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
7
|
+
|
|
8
|
+
interface CorsResponse {
|
|
9
|
+
setHeader(name: string, value: string): unknown;
|
|
10
|
+
vary(field: string): unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setCredentialedExportCorsHeaders(
|
|
14
|
+
origin: unknown,
|
|
15
|
+
response: CorsResponse,
|
|
16
|
+
): void {
|
|
17
|
+
if (typeof origin !== 'string' || !origin) return;
|
|
18
|
+
|
|
19
|
+
response.setHeader('Access-Control-Allow-Origin', origin);
|
|
20
|
+
response.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
21
|
+
response.vary('Origin');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const credentialedExportCorsInterceptor: NestInterceptor = {
|
|
25
|
+
intercept(context, next) {
|
|
26
|
+
const request = context.switchToHttp().getRequest();
|
|
27
|
+
const response = context.switchToHttp().getResponse();
|
|
28
|
+
|
|
29
|
+
setCredentialedExportCorsHeaders(request.headers.origin, response);
|
|
30
|
+
return next.handle();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Public export endpoints authenticate with their short-lived transfer token,
|
|
36
|
+
* while the browser may include its gateway cookie from any deployment origin.
|
|
37
|
+
*/
|
|
38
|
+
export const AllowTransferExportRequest = () =>
|
|
39
|
+
applyDecorators(
|
|
40
|
+
AllowUnauthorizedRequest(),
|
|
41
|
+
UseInterceptors(credentialedExportCorsInterceptor),
|
|
42
|
+
);
|
|
@@ -46,9 +46,9 @@ import {
|
|
|
46
46
|
} from '../../generated/resource/dto/resource-types.dto';
|
|
47
47
|
import { UpdateResourceDto } from '../../generated/resource/dto/update-resource.dto';
|
|
48
48
|
import { ResourceEntity } from '../../generated/resource/entities/resource.entity';
|
|
49
|
-
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
50
49
|
import { ImportPayloadDto } from '../permTransfer/perm-transfer.dto';
|
|
51
50
|
import { ResourceTransferService } from '../permTransfer/resource-transfer.service';
|
|
51
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
52
52
|
import { ResourceService } from './resource.service';
|
|
53
53
|
|
|
54
54
|
import { ConfigService } from '@nestjs/config';
|
|
@@ -194,7 +194,7 @@ export class ResourceController {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
@Get('export/:code')
|
|
197
|
-
@
|
|
197
|
+
@AllowTransferExportRequest()
|
|
198
198
|
async getExportPayload(
|
|
199
199
|
@Param('code') code: string,
|
|
200
200
|
@Query('token') token: string,
|
|
@@ -46,9 +46,9 @@ import {
|
|
|
46
46
|
} from '../../generated/role/dto/role-types.dto';
|
|
47
47
|
import { UpdateRoleDto } from '../../generated/role/dto/update-role.dto';
|
|
48
48
|
import { RoleEntity } from '../../generated/role/entities/role.entity';
|
|
49
|
-
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
50
49
|
import { ImportPayloadDto } from '../permTransfer/perm-transfer.dto';
|
|
51
50
|
import { RoleTransferService } from '../permTransfer/role-transfer.service';
|
|
51
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
52
52
|
import { RoleService } from './role.service';
|
|
53
53
|
|
|
54
54
|
import { ConfigService } from '@nestjs/config';
|
|
@@ -208,7 +208,7 @@ export class RoleController {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
@Get('export/:name')
|
|
211
|
-
@
|
|
211
|
+
@AllowTransferExportRequest()
|
|
212
212
|
async getExportPayload(
|
|
213
213
|
@Param('name') name: string,
|
|
214
214
|
@Query('token') token: string,
|
|
@@ -46,8 +46,8 @@ import {
|
|
|
46
46
|
UserWhereSelect,
|
|
47
47
|
} from '../../generated/user/dto/user-types.dto';
|
|
48
48
|
import { UserEntity } from '../../generated/user/entities/user.entity';
|
|
49
|
-
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
50
49
|
import { ImportPayloadDto } from '../permTransfer/perm-transfer.dto';
|
|
50
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
51
51
|
import { UserTransferService } from '../permTransfer/user-transfer.service';
|
|
52
52
|
import { UserService } from './user.service';
|
|
53
53
|
|
|
@@ -207,7 +207,7 @@ export class UserController {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
@Get('export/:userid')
|
|
210
|
-
@
|
|
210
|
+
@AllowTransferExportRequest()
|
|
211
211
|
async getExportPayload(
|
|
212
212
|
@Param('userid') userid: string,
|
|
213
213
|
@Query('token') token: string,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REQUIRED_USER_GROUP_KEY,
|
|
3
|
+
RequireUserGroup,
|
|
4
|
+
} from './require-user-group.decorator';
|
|
5
|
+
|
|
6
|
+
describe('RequireUserGroup', () => {
|
|
7
|
+
it('stores a normalized group code as route metadata', () => {
|
|
8
|
+
class Controller {
|
|
9
|
+
@RequireUserGroup(' it_ops ')
|
|
10
|
+
create() {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
Reflect.getMetadata(REQUIRED_USER_GROUP_KEY, Controller.prototype.create),
|
|
15
|
+
).toBe('it_ops');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rejects an empty group code instead of disabling authorization', () => {
|
|
19
|
+
expect(() => RequireUserGroup(' ')).toThrow(
|
|
20
|
+
'RequireUserGroup requires a non-empty group code',
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
export const REQUIRED_USER_GROUP_KEY = 'required_user_group';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Restricts an endpoint to authenticated users resolved from the given active
|
|
7
|
+
* user group. Nested active child groups are included by UserGroupService.
|
|
8
|
+
*/
|
|
9
|
+
export function RequireUserGroup(groupCode: string) {
|
|
10
|
+
const normalizedGroupCode = groupCode.trim();
|
|
11
|
+
if (!normalizedGroupCode) {
|
|
12
|
+
throw new Error('RequireUserGroup requires a non-empty group code');
|
|
13
|
+
}
|
|
14
|
+
return SetMetadata(REQUIRED_USER_GROUP_KEY, normalizedGroupCode);
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutionContext,
|
|
3
|
+
ForbiddenException,
|
|
4
|
+
UnauthorizedException,
|
|
5
|
+
} from '@nestjs/common';
|
|
6
|
+
import { Reflector } from '@nestjs/core';
|
|
7
|
+
import { UserGroupDomainError } from './user-group.errors';
|
|
8
|
+
import { UserGroupGuard } from './user-group.guard';
|
|
9
|
+
import { UserGroupService } from './user-group.service';
|
|
10
|
+
|
|
11
|
+
function context(user?: { userid?: string | number; username?: string }) {
|
|
12
|
+
return {
|
|
13
|
+
getHandler: () => function handler() {},
|
|
14
|
+
getClass: () => class Controller {},
|
|
15
|
+
switchToHttp: () => ({ getRequest: () => ({ user }) }),
|
|
16
|
+
} as unknown as ExecutionContext;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('UserGroupGuard', () => {
|
|
20
|
+
const reflector = {
|
|
21
|
+
getAllAndOverride: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
const groups = {
|
|
24
|
+
isUserInGroup: jest.fn(),
|
|
25
|
+
};
|
|
26
|
+
const guard = new UserGroupGuard(
|
|
27
|
+
reflector as unknown as Reflector,
|
|
28
|
+
groups as unknown as UserGroupService,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
beforeEach(() => jest.clearAllMocks());
|
|
32
|
+
|
|
33
|
+
it('allows endpoints without a required user group', async () => {
|
|
34
|
+
reflector.getAllAndOverride.mockReturnValue(undefined);
|
|
35
|
+
|
|
36
|
+
await expect(guard.canActivate(context())).resolves.toBe(true);
|
|
37
|
+
expect(groups.isUserInGroup).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('allows an authenticated member of the required group', async () => {
|
|
41
|
+
reflector.getAllAndOverride.mockReturnValue('it_ops');
|
|
42
|
+
groups.isUserInGroup.mockResolvedValue(true);
|
|
43
|
+
|
|
44
|
+
await expect(guard.canActivate(context({ userid: 'alice' }))).resolves.toBe(
|
|
45
|
+
true,
|
|
46
|
+
);
|
|
47
|
+
expect(groups.isUserInGroup).toHaveBeenCalledWith('it_ops', 'alice');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to username when userid is unavailable', async () => {
|
|
51
|
+
reflector.getAllAndOverride.mockReturnValue('it_ops');
|
|
52
|
+
groups.isUserInGroup.mockResolvedValue(true);
|
|
53
|
+
|
|
54
|
+
await guard.canActivate(context({ username: 'alice' }));
|
|
55
|
+
|
|
56
|
+
expect(groups.isUserInGroup).toHaveBeenCalledWith('it_ops', 'alice');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects a request without an authenticated identity', async () => {
|
|
60
|
+
reflector.getAllAndOverride.mockReturnValue('it_ops');
|
|
61
|
+
|
|
62
|
+
await expect(guard.canActivate(context())).rejects.toBeInstanceOf(
|
|
63
|
+
UnauthorizedException,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects a user outside the required group', async () => {
|
|
68
|
+
reflector.getAllAndOverride.mockReturnValue('it_ops');
|
|
69
|
+
groups.isUserInGroup.mockResolvedValue(false);
|
|
70
|
+
|
|
71
|
+
await expect(
|
|
72
|
+
guard.canActivate(context({ userid: 'bob' })),
|
|
73
|
+
).rejects.toBeInstanceOf(ForbiddenException);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('fails closed when the configured group is missing', async () => {
|
|
77
|
+
reflector.getAllAndOverride.mockReturnValue('missing');
|
|
78
|
+
groups.isUserInGroup.mockRejectedValue(
|
|
79
|
+
new UserGroupDomainError(
|
|
80
|
+
'USER_GROUP_NOT_FOUND',
|
|
81
|
+
'User group not found',
|
|
82
|
+
404,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await expect(
|
|
87
|
+
guard.canActivate(context({ userid: 'alice' })),
|
|
88
|
+
).rejects.toBeInstanceOf(ForbiddenException);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
Injectable,
|
|
6
|
+
UnauthorizedException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Reflector } from '@nestjs/core';
|
|
9
|
+
import { Request } from 'express';
|
|
10
|
+
import { UserGroupDomainError } from './user-group.errors';
|
|
11
|
+
import { REQUIRED_USER_GROUP_KEY } from './require-user-group.decorator';
|
|
12
|
+
import { UserGroupService } from './user-group.service';
|
|
13
|
+
|
|
14
|
+
type AuthenticatedRequest = Request & {
|
|
15
|
+
user?: {
|
|
16
|
+
userid?: string | number;
|
|
17
|
+
username?: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class UserGroupGuard implements CanActivate {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly reflector: Reflector,
|
|
25
|
+
private readonly groups: UserGroupService,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
29
|
+
const groupCode = this.reflector.getAllAndOverride<string>(
|
|
30
|
+
REQUIRED_USER_GROUP_KEY,
|
|
31
|
+
[context.getHandler(), context.getClass()],
|
|
32
|
+
);
|
|
33
|
+
if (!groupCode) return true;
|
|
34
|
+
|
|
35
|
+
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
|
36
|
+
const userId = request.user?.userid ?? request.user?.username;
|
|
37
|
+
if (userId === undefined || String(userId).trim() === '') {
|
|
38
|
+
throw new UnauthorizedException('Authenticated user is required');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (await this.groups.isUserInGroup(groupCode, String(userId))) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!(error instanceof UserGroupDomainError)) throw error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new ForbiddenException('User is not a member of the required group');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -78,6 +78,24 @@ describe('UserGroupService', () => {
|
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it('checks membership using resolved active users', async () => {
|
|
82
|
+
const { service } = createHarness();
|
|
83
|
+
jest
|
|
84
|
+
.spyOn(service, 'resolveUsers')
|
|
85
|
+
.mockResolvedValue([{ userid: 'alice', username: 'Alice' }]);
|
|
86
|
+
|
|
87
|
+
await expect(service.isUserInGroup('finance', 'alice')).resolves.toBe(true);
|
|
88
|
+
await expect(service.isUserInGroup('finance', 'bob')).resolves.toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects blank membership identities without resolving the group', async () => {
|
|
92
|
+
const { service } = createHarness();
|
|
93
|
+
const resolveUsers = jest.spyOn(service, 'resolveUsers');
|
|
94
|
+
|
|
95
|
+
await expect(service.isUserInGroup('finance', ' ')).resolves.toBe(false);
|
|
96
|
+
expect(resolveUsers).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
81
99
|
it('replaces members and bumps revision', async () => {
|
|
82
100
|
const { service, tx } = createHarness();
|
|
83
101
|
tx.user.findMany.mockResolvedValue([{ userid: 'u1' }]);
|
|
@@ -334,6 +334,13 @@ export class UserGroupService {
|
|
|
334
334
|
return [...users.values()].sort((a, b) => a.userid.localeCompare(b.userid));
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
async isUserInGroup(code: string, userId: string): Promise<boolean> {
|
|
338
|
+
const normalizedUserId = userId.trim();
|
|
339
|
+
if (!normalizedUserId) return false;
|
|
340
|
+
const users = await this.resolveUsers(code);
|
|
341
|
+
return users.some((user) => user.userid === normalizedUserId);
|
|
342
|
+
}
|
|
343
|
+
|
|
337
344
|
private async findGroup(tx: Transaction, code: string) {
|
|
338
345
|
const group = await tx.userGroup.findUnique({ where: { code } });
|
|
339
346
|
if (!group) throw userGroupNotFound(code);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { UserGroupController } from './user-group.controller';
|
|
3
|
+
import { UserGroupGuard } from './user-group.guard';
|
|
3
4
|
import { UserGroupService } from './user-group.service';
|
|
4
5
|
|
|
5
6
|
@Module({
|
|
6
7
|
controllers: [UserGroupController],
|
|
7
|
-
providers: [UserGroupService],
|
|
8
|
-
exports: [UserGroupService],
|
|
8
|
+
providers: [UserGroupService, UserGroupGuard],
|
|
9
|
+
exports: [UserGroupService, UserGroupGuard],
|
|
9
10
|
})
|
|
10
11
|
export class UserGroupModule {}
|
|
@@ -24,6 +24,7 @@ import { Request } from 'express';
|
|
|
24
24
|
import { UseRoles } from 'nest-access-control';
|
|
25
25
|
import { PrismaService } from 'nestjs-prisma';
|
|
26
26
|
import { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
27
|
+
import { AllowTransferExportRequest } from '../permTransfer/transfer-export.decorator';
|
|
27
28
|
import { TemporalService } from './temporal.service';
|
|
28
29
|
import { listWorkflowDatabaseFunctions } from './workflow-database-function';
|
|
29
30
|
import {
|
|
@@ -196,7 +197,7 @@ export class WorkflowController {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
@Get('export/:code')
|
|
199
|
-
@
|
|
200
|
+
@AllowTransferExportRequest()
|
|
200
201
|
async getExportPayload(
|
|
201
202
|
@Param('code') code: string,
|
|
202
203
|
@Query('token') token: string,
|
|
@@ -96,7 +96,7 @@ export default defineConfig(({ mode }) => {
|
|
|
96
96
|
'**/.vite/**',
|
|
97
97
|
// 生成物不参与 HMR:分发期间会有几百个 add/change 事件,
|
|
98
98
|
// 若不 ignore 会触发大量级联失效。改由 gadminRestartPlugin
|
|
99
|
-
// 在分发全部完成后一次性
|
|
99
|
+
// 在分发全部完成后一次性 invalidate + full-reload(见文件顶部说明)。
|
|
100
100
|
'**/src/generated/**',
|
|
101
101
|
],
|
|
102
102
|
awaitWriteFinish: {
|