@gadmin2n/schematics 0.0.150 → 0.0.152

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 (82) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +125 -36
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
  79. package/package.json +1 -1
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +0 -8
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
@@ -0,0 +1,28 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { PageTransferService } from './page-transfer.service';
3
+ import { ResourceTransferService } from './resource-transfer.service';
4
+ import { RoleTransferService } from './role-transfer.service';
5
+ import { TransferTokenService } from './transfer-token.service';
6
+ import { UserTransferService } from './user-transfer.service';
7
+
8
+ /**
9
+ * Cross-environment import/export for permission data
10
+ * (user / role / page / resource). Core logic lives here;
11
+ * each entity's controller only declares thin endpoints.
12
+ */
13
+ @Module({
14
+ providers: [
15
+ TransferTokenService,
16
+ UserTransferService,
17
+ RoleTransferService,
18
+ PageTransferService,
19
+ ResourceTransferService,
20
+ ],
21
+ exports: [
22
+ UserTransferService,
23
+ RoleTransferService,
24
+ PageTransferService,
25
+ ResourceTransferService,
26
+ ],
27
+ })
28
+ export class PermTransferModule {}
@@ -0,0 +1,137 @@
1
+ import { Inject, Injectable, NotFoundException } from '@nestjs/common';
2
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
3
+ import { PrismaService } from 'nestjs-prisma';
4
+ import * as path from 'path';
5
+ import { Logger } from 'winston';
6
+ import {
7
+ assertTransferPayload,
8
+ ImportPreviewResponse,
9
+ ResourceExportPayload,
10
+ TRANSFER_PAYLOAD_VERSION,
11
+ TransferPayload,
12
+ } from './perm-transfer.dto';
13
+ import { TransferTokenService } from './transfer-token.service';
14
+
15
+ @Injectable()
16
+ export class ResourceTransferService {
17
+ constructor(
18
+ private prisma: PrismaService,
19
+ private readonly tokenService: TransferTokenService,
20
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
21
+ ) {
22
+ this.logger = this.logger.child({ file: path.basename(__filename) });
23
+ }
24
+
25
+ async generateExportToken(
26
+ code: string,
27
+ baseUrl: string,
28
+ ): Promise<{ url: string }> {
29
+ const resource = await this.prisma.resource.findUnique({
30
+ where: { code },
31
+ select: { code: true },
32
+ });
33
+ if (!resource) throw new NotFoundException('Resource not found');
34
+
35
+ const token = this.tokenService.sign('resource', code);
36
+ return {
37
+ url: this.tokenService.buildUrl(baseUrl, 'resource', code, token),
38
+ };
39
+ }
40
+
41
+ async getExportPayload(
42
+ code: string,
43
+ token: string,
44
+ ): Promise<ResourceExportPayload> {
45
+ this.tokenService.verify(token, 'resource', code);
46
+
47
+ const resource = await this.prisma.resource.findUnique({
48
+ where: { code },
49
+ });
50
+ if (!resource) throw new NotFoundException('Resource not found');
51
+
52
+ return {
53
+ version: TRANSFER_PAYLOAD_VERSION,
54
+ exportedAt: new Date().toISOString(),
55
+ entity: 'resource',
56
+ resource: {
57
+ code: resource.code,
58
+ name: resource.name,
59
+ description: resource.description,
60
+ isCommonResource: resource.isCommonResource,
61
+ },
62
+ };
63
+ }
64
+
65
+ async importPreview(
66
+ payload: TransferPayload,
67
+ ): Promise<ImportPreviewResponse> {
68
+ assertTransferPayload(payload, 'resource');
69
+ const incoming = (payload as ResourceExportPayload).resource;
70
+
71
+ const current = await this.prisma.resource.findUnique({
72
+ where: { code: incoming.code },
73
+ });
74
+
75
+ // code/name are both unique — detect a name clash on a different record
76
+ const conflicts: string[] = [];
77
+ const nameOwner = await this.prisma.resource.findUnique({
78
+ where: { name: incoming.name },
79
+ select: { code: true },
80
+ });
81
+ if (nameOwner && nameOwner.code !== incoming.code) {
82
+ conflicts.push(
83
+ `name "${incoming.name}" is already used by resource "${nameOwner.code}"`,
84
+ );
85
+ }
86
+
87
+ return {
88
+ current: current
89
+ ? {
90
+ resource: {
91
+ code: current.code,
92
+ name: current.name,
93
+ description: current.description,
94
+ isCommonResource: current.isCommonResource,
95
+ },
96
+ }
97
+ : null,
98
+ incoming: { resource: incoming },
99
+ missingRefs: [],
100
+ conflicts,
101
+ };
102
+ }
103
+
104
+ async importConfirm(
105
+ payload: TransferPayload,
106
+ operator: string,
107
+ ): Promise<{ success: true; key: string }> {
108
+ assertTransferPayload(payload, 'resource');
109
+ const incoming = (payload as ResourceExportPayload).resource;
110
+
111
+ this.logger.info(
112
+ `[permTransfer] resource import_confirm - code: ${incoming.code}, operator: ${operator}`,
113
+ );
114
+ await this.prisma.$transaction(async (tx) => {
115
+ await tx.resource.upsert({
116
+ where: { code: incoming.code },
117
+ create: { ...incoming, creator: operator },
118
+ update: incoming,
119
+ });
120
+ await tx.audit.create({
121
+ data: {
122
+ creator: operator,
123
+ authorName: operator,
124
+ action: 'IMPORT_RESOURCE',
125
+ resource: 'resource',
126
+ resourceId: incoming.code,
127
+ data: JSON.stringify({
128
+ code: incoming.code,
129
+ name: incoming.name,
130
+ isCommonResource: incoming.isCommonResource,
131
+ }),
132
+ },
133
+ });
134
+ });
135
+ return { success: true, key: incoming.code };
136
+ }
137
+ }
@@ -0,0 +1,248 @@
1
+ import {
2
+ BadRequestException,
3
+ Inject,
4
+ Injectable,
5
+ NotFoundException,
6
+ } from '@nestjs/common';
7
+ import { Prisma } from '@prisma/client';
8
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
9
+ import { PrismaService } from 'nestjs-prisma';
10
+ import * as path from 'path';
11
+ import { Logger } from 'winston';
12
+ import {
13
+ assertTransferPayload,
14
+ ImportPreviewResponse,
15
+ MissingRef,
16
+ RoleExportPayload,
17
+ RoleResourceTransferItem,
18
+ TRANSFER_PAYLOAD_VERSION,
19
+ TransferPayload,
20
+ } from './perm-transfer.dto';
21
+ import { TransferTokenService } from './transfer-token.service';
22
+
23
+ @Injectable()
24
+ export class RoleTransferService {
25
+ constructor(
26
+ private prisma: PrismaService,
27
+ private readonly tokenService: TransferTokenService,
28
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
29
+ ) {
30
+ this.logger = this.logger.child({ file: path.basename(__filename) });
31
+ }
32
+
33
+ async generateExportToken(
34
+ name: string,
35
+ baseUrl: string,
36
+ ): Promise<{ url: string }> {
37
+ const role = await this.prisma.role.findUnique({
38
+ where: { name },
39
+ select: { name: true },
40
+ });
41
+ if (!role) throw new NotFoundException('Role not found');
42
+
43
+ const token = this.tokenService.sign('role', name);
44
+ return { url: this.tokenService.buildUrl(baseUrl, 'role', name, token) };
45
+ }
46
+
47
+ private async assemble(
48
+ name: string,
49
+ client: Prisma.TransactionClient | PrismaService = this.prisma,
50
+ ): Promise<{
51
+ role: { name: string; description: string | null };
52
+ rolePages: string[];
53
+ roleResources: RoleResourceTransferItem[];
54
+ } | null> {
55
+ const role = await client.role.findUnique({
56
+ where: { name },
57
+ include: {
58
+ rolePages: { include: { page: { select: { code: true } } } },
59
+ roleResources: { include: { resource: { select: { code: true } } } },
60
+ },
61
+ });
62
+ if (!role) return null;
63
+
64
+ return {
65
+ role: { name: role.name, description: role.description },
66
+ rolePages: role.rolePages.map((rp) => rp.page.code).sort(),
67
+ roleResources: role.roleResources
68
+ .map((rr) => ({
69
+ resourceCode: rr.resource.code,
70
+ actions: rr.actions,
71
+ }))
72
+ .sort((a, b) => a.resourceCode.localeCompare(b.resourceCode)),
73
+ };
74
+ }
75
+
76
+ async getExportPayload(
77
+ name: string,
78
+ token: string,
79
+ ): Promise<RoleExportPayload> {
80
+ this.tokenService.verify(token, 'role', name);
81
+
82
+ const data = await this.assemble(name);
83
+ if (!data) throw new NotFoundException('Role not found');
84
+
85
+ return {
86
+ version: TRANSFER_PAYLOAD_VERSION,
87
+ exportedAt: new Date().toISOString(),
88
+ entity: 'role',
89
+ ...data,
90
+ };
91
+ }
92
+
93
+ private async findMissingRefs(
94
+ rolePages: string[],
95
+ roleResources: RoleResourceTransferItem[],
96
+ ): Promise<MissingRef[]> {
97
+ const pageCodes = [...new Set(rolePages)];
98
+ const resourceCodes = [
99
+ ...new Set(roleResources.map((r) => r.resourceCode)),
100
+ ];
101
+ const [pages, resources] = await Promise.all([
102
+ pageCodes.length
103
+ ? this.prisma.page.findMany({
104
+ where: { code: { in: pageCodes } },
105
+ select: { code: true },
106
+ })
107
+ : [],
108
+ resourceCodes.length
109
+ ? this.prisma.resource.findMany({
110
+ where: { code: { in: resourceCodes } },
111
+ select: { code: true },
112
+ })
113
+ : [],
114
+ ]);
115
+ const existingPages = new Set(pages.map((p) => p.code));
116
+ const existingResources = new Set(resources.map((r) => r.code));
117
+
118
+ return [
119
+ ...pageCodes
120
+ .filter((c) => !existingPages.has(c))
121
+ .map((code): MissingRef => ({ type: 'page', code })),
122
+ ...resourceCodes
123
+ .filter((c) => !existingResources.has(c))
124
+ .map((code): MissingRef => ({ type: 'resource', code })),
125
+ ];
126
+ }
127
+
128
+ async importPreview(
129
+ payload: TransferPayload,
130
+ ): Promise<ImportPreviewResponse> {
131
+ assertTransferPayload(payload, 'role');
132
+ const { role, rolePages, roleResources } = payload as RoleExportPayload;
133
+
134
+ const current = await this.assemble(role.name);
135
+ const missingRefs = await this.findMissingRefs(rolePages, roleResources);
136
+
137
+ return {
138
+ current,
139
+ incoming: { role, rolePages, roleResources },
140
+ missingRefs,
141
+ conflicts: [],
142
+ };
143
+ }
144
+
145
+ async importConfirm(
146
+ payload: TransferPayload,
147
+ operator: string,
148
+ ): Promise<{ success: true; key: string }> {
149
+ assertTransferPayload(payload, 'role');
150
+ const { role, rolePages, roleResources } = payload as RoleExportPayload;
151
+
152
+ this.logger.info(
153
+ `[permTransfer] role import_confirm - name: ${role.name}, operator: ${operator}`,
154
+ );
155
+ await this.prisma.$transaction(async (tx) => {
156
+ const saved = await tx.role.upsert({
157
+ where: { name: role.name },
158
+ create: {
159
+ name: role.name,
160
+ description: role.description,
161
+ creator: operator,
162
+ },
163
+ update: { description: role.description },
164
+ select: { id: true },
165
+ });
166
+
167
+ // resolve refs inside the transaction; missing → throw → rollback
168
+ const pageCodes = [...new Set(rolePages)];
169
+ const resourceCodes = [
170
+ ...new Set(roleResources.map((r) => r.resourceCode)),
171
+ ];
172
+ const [pages, resources] = await Promise.all([
173
+ pageCodes.length
174
+ ? tx.page.findMany({
175
+ where: { code: { in: pageCodes } },
176
+ select: { id: true, code: true },
177
+ })
178
+ : [],
179
+ resourceCodes.length
180
+ ? tx.resource.findMany({
181
+ where: { code: { in: resourceCodes } },
182
+ select: { id: true, code: true },
183
+ })
184
+ : [],
185
+ ]);
186
+ const pageIdByCode = new Map<string, number>(
187
+ pages.map((p) => [p.code, p.id] as [string, number]),
188
+ );
189
+ const resourceIdByCode = new Map<string, number>(
190
+ resources.map((r) => [r.code, r.id] as [string, number]),
191
+ );
192
+ const missingPage = pageCodes.find((c) => !pageIdByCode.has(c));
193
+ if (missingPage) {
194
+ throw new BadRequestException(
195
+ `Referenced page does not exist: ${missingPage} (import it first)`,
196
+ );
197
+ }
198
+ const missingResource = resourceCodes.find(
199
+ (c) => !resourceIdByCode.has(c),
200
+ );
201
+ if (missingResource) {
202
+ throw new BadRequestException(
203
+ `Referenced resource does not exist: ${missingResource} (import it first)`,
204
+ );
205
+ }
206
+
207
+ // full overwrite: wipe associations, then rebuild from the payload
208
+ await tx.rolePages.deleteMany({ where: { roleId: saved.id } });
209
+ await tx.roleResource.deleteMany({ where: { roleId: saved.id } });
210
+ if (pageCodes.length > 0) {
211
+ await tx.rolePages.createMany({
212
+ data: pageCodes.map((code) => ({
213
+ roleId: saved.id,
214
+ pageId: pageIdByCode.get(code)!,
215
+ creator: operator,
216
+ })),
217
+ });
218
+ }
219
+ if (roleResources.length > 0) {
220
+ await tx.roleResource.createMany({
221
+ data: roleResources.map((rr) => ({
222
+ roleId: saved.id,
223
+ resourceId: resourceIdByCode.get(rr.resourceCode)!,
224
+ actions: rr.actions as Prisma.InputJsonValue,
225
+ creator: operator,
226
+ })),
227
+ });
228
+ }
229
+
230
+ await tx.audit.create({
231
+ data: {
232
+ creator: operator,
233
+ authorName: operator,
234
+ action: 'IMPORT_ROLE',
235
+ resource: 'role',
236
+ resourceId: role.name,
237
+ data: JSON.stringify({
238
+ name: role.name,
239
+ rolePages,
240
+ roleResources: roleResources.map((rr) => rr.resourceCode),
241
+ }),
242
+ },
243
+ });
244
+ });
245
+
246
+ return { success: true, key: role.name };
247
+ }
248
+ }
@@ -0,0 +1,65 @@
1
+ import {
2
+ Injectable,
3
+ InternalServerErrorException,
4
+ UnauthorizedException,
5
+ } from '@nestjs/common';
6
+ import * as jwt from 'jsonwebtoken';
7
+ import { TransferEntityKind } from './perm-transfer.dto';
8
+
9
+ const EXPORT_TOKEN_EXPIRY = 30 * 60; // 30 minutes in seconds
10
+
11
+ interface ExportTokenPayload {
12
+ entity: TransferEntityKind;
13
+ key: string;
14
+ }
15
+
16
+ function exportTokenSecret(): string {
17
+ const secret = process.env.TAIHU_ODC_APP_TOKEN?.trim();
18
+ if (!secret)
19
+ throw new InternalServerErrorException(
20
+ 'Export token secret is not configured',
21
+ );
22
+ return secret;
23
+ }
24
+
25
+ /**
26
+ * Signs/verifies the short-lived JWT that authorizes pulling an export
27
+ * payload from a source environment. Mirrors the workflow export token
28
+ * mechanism (workflow-export.service.ts).
29
+ */
30
+ @Injectable()
31
+ export class TransferTokenService {
32
+ sign(entity: TransferEntityKind, key: string): string {
33
+ return jwt.sign(
34
+ { entity, key } as ExportTokenPayload,
35
+ exportTokenSecret(),
36
+ {
37
+ expiresIn: EXPORT_TOKEN_EXPIRY,
38
+ },
39
+ );
40
+ }
41
+
42
+ /** @throws UnauthorizedException when invalid/expired or entity/key mismatch */
43
+ verify(token: string, entity: TransferEntityKind, key: string): void {
44
+ let decoded: ExportTokenPayload;
45
+ try {
46
+ decoded = jwt.verify(token, exportTokenSecret()) as ExportTokenPayload;
47
+ } catch {
48
+ throw new UnauthorizedException('Export token is invalid or expired');
49
+ }
50
+ if (decoded.entity !== entity || decoded.key !== key) {
51
+ throw new UnauthorizedException('Token does not match this export');
52
+ }
53
+ }
54
+
55
+ buildUrl(
56
+ baseUrl: string,
57
+ entity: TransferEntityKind,
58
+ key: string,
59
+ token: string,
60
+ ): string {
61
+ return `${baseUrl}/${entity}/export/${encodeURIComponent(
62
+ key,
63
+ )}?token=${token}`;
64
+ }
65
+ }
@@ -0,0 +1,146 @@
1
+ import { Inject, Injectable, NotFoundException } from '@nestjs/common';
2
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
3
+ import { PrismaService } from 'nestjs-prisma';
4
+ import * as path from 'path';
5
+ import { Logger } from 'winston';
6
+ import {
7
+ assertTransferPayload,
8
+ ImportPreviewResponse,
9
+ TRANSFER_PAYLOAD_VERSION,
10
+ TransferPayload,
11
+ UserExportPayload,
12
+ UserTransferData,
13
+ } from './perm-transfer.dto';
14
+ import { TransferTokenService } from './transfer-token.service';
15
+
16
+ /** Stored as a JSON string in t_admin_user_role.roles; tolerate dirty data. */
17
+ function parseRoles(raw: string): unknown {
18
+ try {
19
+ return JSON.parse(raw);
20
+ } catch {
21
+ return raw;
22
+ }
23
+ }
24
+
25
+ function stringifyRoles(roles: unknown): string {
26
+ return typeof roles === 'string' ? roles : JSON.stringify(roles ?? []);
27
+ }
28
+
29
+ @Injectable()
30
+ export class UserTransferService {
31
+ constructor(
32
+ private prisma: PrismaService,
33
+ private readonly tokenService: TransferTokenService,
34
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
35
+ ) {
36
+ this.logger = this.logger.child({ file: path.basename(__filename) });
37
+ }
38
+
39
+ async generateExportToken(
40
+ userid: string,
41
+ baseUrl: string,
42
+ ): Promise<{ url: string }> {
43
+ const user = await this.prisma.user.findUnique({
44
+ where: { userid },
45
+ select: { userid: true },
46
+ });
47
+ if (!user) throw new NotFoundException('User not found');
48
+
49
+ const token = this.tokenService.sign('user', userid);
50
+ return { url: this.tokenService.buildUrl(baseUrl, 'user', userid, token) };
51
+ }
52
+
53
+ async getExportPayload(
54
+ userid: string,
55
+ token: string,
56
+ ): Promise<UserExportPayload> {
57
+ this.tokenService.verify(token, 'user', userid);
58
+
59
+ const user = await this.prisma.user.findUnique({ where: { userid } });
60
+ if (!user) throw new NotFoundException('User not found');
61
+
62
+ return {
63
+ version: TRANSFER_PAYLOAD_VERSION,
64
+ exportedAt: new Date().toISOString(),
65
+ entity: 'user',
66
+ user: {
67
+ userid: user.userid,
68
+ username: user.username,
69
+ roles: parseRoles(user.roles),
70
+ description: user.description,
71
+ isActive: user.isActive,
72
+ },
73
+ };
74
+ }
75
+
76
+ private toComparable(user: UserTransferData) {
77
+ return { user };
78
+ }
79
+
80
+ async importPreview(
81
+ payload: TransferPayload,
82
+ ): Promise<ImportPreviewResponse> {
83
+ assertTransferPayload(payload, 'user');
84
+ const incoming = (payload as UserExportPayload).user;
85
+
86
+ const current = await this.prisma.user.findUnique({
87
+ where: { userid: incoming.userid },
88
+ });
89
+
90
+ return {
91
+ current: current
92
+ ? this.toComparable({
93
+ userid: current.userid,
94
+ username: current.username,
95
+ roles: parseRoles(current.roles),
96
+ description: current.description,
97
+ isActive: current.isActive,
98
+ })
99
+ : null,
100
+ incoming: this.toComparable(incoming),
101
+ missingRefs: [],
102
+ conflicts: [],
103
+ };
104
+ }
105
+
106
+ async importConfirm(
107
+ payload: TransferPayload,
108
+ operator: string,
109
+ ): Promise<{ success: true; key: string }> {
110
+ assertTransferPayload(payload, 'user');
111
+ const incoming = (payload as UserExportPayload).user;
112
+
113
+ this.logger.info(
114
+ `[permTransfer] user import_confirm - userid: ${incoming.userid}, operator: ${operator}`,
115
+ );
116
+ const data = {
117
+ username: incoming.username,
118
+ roles: stringifyRoles(incoming.roles),
119
+ description: incoming.description,
120
+ isActive: incoming.isActive,
121
+ };
122
+ await this.prisma.$transaction(async (tx) => {
123
+ await tx.user.upsert({
124
+ where: { userid: incoming.userid },
125
+ create: { userid: incoming.userid, ...data, creator: operator },
126
+ update: data,
127
+ });
128
+ await tx.audit.create({
129
+ data: {
130
+ creator: operator,
131
+ authorName: operator,
132
+ action: 'IMPORT_USER',
133
+ resource: 'user',
134
+ resourceId: incoming.userid,
135
+ data: JSON.stringify({
136
+ userid: incoming.userid,
137
+ username: incoming.username,
138
+ roles: incoming.roles,
139
+ isActive: incoming.isActive,
140
+ }),
141
+ },
142
+ });
143
+ });
144
+ return { success: true, key: incoming.userid };
145
+ }
146
+ }