@hed-hog/core 0.0.300 → 0.0.302
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/ai/ai.service.d.ts +13 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +104 -2
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +26 -9
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +11 -5
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +34 -10
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +196 -69
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/services/integration-link.service.d.ts +5 -1
- package/dist/integration/services/integration-link.service.d.ts.map +1 -1
- package/dist/integration/services/integration-link.service.js +141 -53
- package/dist/integration/services/integration-link.service.js.map +1 -1
- package/dist/mail/mail.service.d.ts +9 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +56 -4
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts +6 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +188 -15
- package/dist/setting/setting.service.js.map +1 -1
- package/hedhog/data/setting_group.yaml +28 -0
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +305 -75
- package/hedhog/frontend/messages/en.json +15 -3
- package/hedhog/frontend/messages/pt.json +15 -3
- package/package.json +5 -5
- package/src/ai/ai.service.ts +129 -1
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +9 -2
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +276 -75
- package/src/index.ts +7 -6
- package/src/integration/services/integration-link.service.ts +190 -55
- package/src/mail/mail.service.ts +67 -3
- package/src/setting/setting.service.ts +222 -15
package/src/ai/ai.service.ts
CHANGED
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
Injectable,
|
|
8
8
|
Logger,
|
|
9
9
|
NotFoundException,
|
|
10
|
+
OnModuleInit,
|
|
10
11
|
} from '@nestjs/common';
|
|
11
12
|
import axios from 'axios';
|
|
12
13
|
import { createHash } from 'crypto';
|
|
13
14
|
import pdfParse from 'pdf-parse';
|
|
14
15
|
import { DeleteDTO } from '../dto/delete.dto';
|
|
15
16
|
import { FileService } from '../file/file.service';
|
|
17
|
+
import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
|
|
16
18
|
import { SettingService } from '../setting/setting.service';
|
|
17
19
|
import { ChatAgentDTO } from './dto/chat-agent.dto';
|
|
18
20
|
import { ChatDTO } from './dto/chat.dto';
|
|
@@ -38,8 +40,10 @@ type AiAttachment = {
|
|
|
38
40
|
buffer: Buffer;
|
|
39
41
|
};
|
|
40
42
|
|
|
43
|
+
type AiProviderKeySettingSlug = 'ai-openai-api-key' | 'ai-gemini-api-key';
|
|
44
|
+
|
|
41
45
|
@Injectable()
|
|
42
|
-
export class AiService {
|
|
46
|
+
export class AiService implements OnModuleInit {
|
|
43
47
|
private readonly logger = new Logger(AiService.name);
|
|
44
48
|
|
|
45
49
|
constructor(
|
|
@@ -49,8 +53,29 @@ export class AiService {
|
|
|
49
53
|
private readonly settingService: SettingService,
|
|
50
54
|
@Inject(forwardRef(() => FileService))
|
|
51
55
|
private readonly fileService: FileService,
|
|
56
|
+
@Inject(forwardRef(() => IntegrationDeveloperApiService))
|
|
57
|
+
private readonly integrationApi: IntegrationDeveloperApiService,
|
|
52
58
|
) {}
|
|
53
59
|
|
|
60
|
+
onModuleInit(): void {
|
|
61
|
+
this.integrationApi.subscribe({
|
|
62
|
+
eventName: 'core.setting.changed',
|
|
63
|
+
consumerName: 'core.ai-provider-key-validator',
|
|
64
|
+
priority: 0,
|
|
65
|
+
handler: async (event) => {
|
|
66
|
+
const slug = String(event.payload?.slug || '').trim();
|
|
67
|
+
|
|
68
|
+
if (!this.isAiProviderKeySlug(slug)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await this.syncProviderEnabledFlag(slug);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
void this.syncAllProviderEnabledFlags();
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
async chat(data: ChatDTO, file?: MulterFile) {
|
|
55
80
|
const provider = data.provider || 'openai';
|
|
56
81
|
const attachment = await this.resolveAttachment(file, data.file_id);
|
|
@@ -398,6 +423,109 @@ export class AiService {
|
|
|
398
423
|
return 'gemini-1.5-flash';
|
|
399
424
|
}
|
|
400
425
|
|
|
426
|
+
private isAiProviderKeySlug(slug: string): slug is AiProviderKeySettingSlug {
|
|
427
|
+
return slug === 'ai-openai-api-key' || slug === 'ai-gemini-api-key';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private getEnabledSlugFromProviderKey(slug: AiProviderKeySettingSlug) {
|
|
431
|
+
return slug === 'ai-openai-api-key'
|
|
432
|
+
? 'ai-openai-api-key-enabled'
|
|
433
|
+
: 'ai-gemini-api-key-enabled';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private async syncAllProviderEnabledFlags() {
|
|
437
|
+
const results = await Promise.allSettled([
|
|
438
|
+
this.syncProviderEnabledFlag('ai-openai-api-key'),
|
|
439
|
+
this.syncProviderEnabledFlag('ai-gemini-api-key'),
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
results.forEach((result, index) => {
|
|
443
|
+
if (result.status === 'rejected') {
|
|
444
|
+
const slug = index === 0 ? 'ai-openai-api-key' : 'ai-gemini-api-key';
|
|
445
|
+
this.logger.warn(
|
|
446
|
+
`Failed to sync provider availability for ${slug}: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async syncProviderEnabledFlag(slug: AiProviderKeySettingSlug) {
|
|
453
|
+
const values = await this.settingService.getSettingValues(slug);
|
|
454
|
+
const apiKey = String(values?.[slug] || '').trim();
|
|
455
|
+
const enabledSlug = this.getEnabledSlugFromProviderKey(slug);
|
|
456
|
+
|
|
457
|
+
const isValid = slug === 'ai-openai-api-key'
|
|
458
|
+
? await this.validateOpenAiKey(apiKey)
|
|
459
|
+
: await this.validateGeminiKey(apiKey);
|
|
460
|
+
|
|
461
|
+
await this.settingService.setValue(enabledSlug, isValid ? 'true' : 'false');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private getValidationErrorMessage(error: unknown) {
|
|
465
|
+
if (axios.isAxiosError(error)) {
|
|
466
|
+
const providerMessage =
|
|
467
|
+
error.response?.data?.error?.message ||
|
|
468
|
+
error.response?.data?.message ||
|
|
469
|
+
error.message;
|
|
470
|
+
const status = error.response?.status || 'network';
|
|
471
|
+
return `${status}: ${providerMessage}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return error instanceof Error ? error.message : String(error);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async validateOpenAiKey(apiKey?: string | null): Promise<boolean> {
|
|
478
|
+
const normalizedKey = String(apiKey || '').trim();
|
|
479
|
+
|
|
480
|
+
if (!normalizedKey) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
await axios.get('https://api.openai.com/v1/models', {
|
|
486
|
+
headers: {
|
|
487
|
+
Authorization: `Bearer ${normalizedKey}`,
|
|
488
|
+
},
|
|
489
|
+
params: {
|
|
490
|
+
limit: 1,
|
|
491
|
+
},
|
|
492
|
+
timeout: 5000,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return true;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
this.logger.warn(
|
|
498
|
+
`OpenAI key validation failed: ${this.getValidationErrorMessage(error)}`,
|
|
499
|
+
);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async validateGeminiKey(apiKey?: string | null): Promise<boolean> {
|
|
505
|
+
const normalizedKey = String(apiKey || '').trim();
|
|
506
|
+
|
|
507
|
+
if (!normalizedKey) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
await axios.get('https://generativelanguage.googleapis.com/v1beta/models', {
|
|
513
|
+
params: {
|
|
514
|
+
key: normalizedKey,
|
|
515
|
+
pageSize: 1,
|
|
516
|
+
},
|
|
517
|
+
timeout: 5000,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return true;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
this.logger.warn(
|
|
523
|
+
`Gemini key validation failed: ${this.getValidationErrorMessage(error)}`,
|
|
524
|
+
);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
401
529
|
private async getApiKeys() {
|
|
402
530
|
const settings = await this.settingService.getSettingValues([
|
|
403
531
|
'ai-openai-api-key',
|
|
@@ -105,12 +105,18 @@ export class DashboardCoreController {
|
|
|
105
105
|
@User() user,
|
|
106
106
|
@Param('slug') slug: string,
|
|
107
107
|
@Query('search') search: string | undefined,
|
|
108
|
+
@Query('page') page: string | undefined,
|
|
109
|
+
@Query('pageSize') pageSize: string | undefined,
|
|
108
110
|
@Locale() locale: string,
|
|
109
111
|
) {
|
|
110
112
|
return this.dashboardCoreService.getShareableUsers(
|
|
111
113
|
user.id,
|
|
112
114
|
slug,
|
|
113
|
-
|
|
115
|
+
{
|
|
116
|
+
search,
|
|
117
|
+
page,
|
|
118
|
+
pageSize,
|
|
119
|
+
},
|
|
114
120
|
locale,
|
|
115
121
|
);
|
|
116
122
|
}
|
|
@@ -119,12 +125,13 @@ export class DashboardCoreController {
|
|
|
119
125
|
shareDashboard(
|
|
120
126
|
@User() user,
|
|
121
127
|
@Param('slug') slug: string,
|
|
122
|
-
@Body() body: { userId?: number },
|
|
128
|
+
@Body() body: { userId?: number; userIds?: number[] },
|
|
123
129
|
@Locale() locale: string,
|
|
124
130
|
) {
|
|
125
131
|
return this.dashboardCoreService.shareDashboard(
|
|
126
132
|
user.id,
|
|
127
133
|
slug,
|
|
134
|
+
body.userIds,
|
|
128
135
|
body.userId,
|
|
129
136
|
locale,
|
|
130
137
|
);
|
|
@@ -722,6 +722,94 @@ export class DashboardCoreService {
|
|
|
722
722
|
return roleUsers.map((roleUser) => roleUser.role_id);
|
|
723
723
|
}
|
|
724
724
|
|
|
725
|
+
private async getDashboardComponentRoleRequirements(dashboardId: number) {
|
|
726
|
+
const dashboardItems = await this.prismaService.dashboard_item.findMany({
|
|
727
|
+
where: {
|
|
728
|
+
dashboard_id: dashboardId,
|
|
729
|
+
},
|
|
730
|
+
select: {
|
|
731
|
+
component_id: true,
|
|
732
|
+
dashboard_component: {
|
|
733
|
+
select: {
|
|
734
|
+
dashboard_component_role: {
|
|
735
|
+
select: {
|
|
736
|
+
role_id: true,
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const uniqueByComponentId = new Map<number, number[]>();
|
|
745
|
+
|
|
746
|
+
for (const item of dashboardItems) {
|
|
747
|
+
if (uniqueByComponentId.has(item.component_id)) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
uniqueByComponentId.set(
|
|
752
|
+
item.component_id,
|
|
753
|
+
item.dashboard_component.dashboard_component_role.map(
|
|
754
|
+
(relation) => relation.role_id,
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return Array.from(uniqueByComponentId.values());
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private userHasRequiredRolesForDashboard(
|
|
763
|
+
componentRoleRequirements: number[][],
|
|
764
|
+
userRoleIds: number[],
|
|
765
|
+
) {
|
|
766
|
+
if (componentRoleRequirements.length === 0) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (userRoleIds.length === 0) {
|
|
771
|
+
return componentRoleRequirements.every(
|
|
772
|
+
(requiredRoles) => requiredRoles.length === 0,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const userRoleIdSet = new Set(userRoleIds);
|
|
777
|
+
|
|
778
|
+
return componentRoleRequirements.every(
|
|
779
|
+
(requiredRoles) =>
|
|
780
|
+
requiredRoles.length === 0 ||
|
|
781
|
+
requiredRoles.some((roleId) => userRoleIdSet.has(roleId)),
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private async getDashboardRoleAccessState(dashboardId: number, userId: number) {
|
|
786
|
+
const [componentRoleRequirements, userRoleIds] = await Promise.all([
|
|
787
|
+
this.getDashboardComponentRoleRequirements(dashboardId),
|
|
788
|
+
this.getUserRoleIds(userId),
|
|
789
|
+
]);
|
|
790
|
+
|
|
791
|
+
const hasRequiredRoles = this.userHasRequiredRolesForDashboard(
|
|
792
|
+
componentRoleRequirements,
|
|
793
|
+
userRoleIds,
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
return {
|
|
797
|
+
hasRequiredRoles,
|
|
798
|
+
accessStatus: hasRequiredRoles ? 'allowed' : 'missing-roles',
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private async assertDashboardRoleAccess(dashboardId: number, userId: number) {
|
|
803
|
+
const { hasRequiredRoles } = await this.getDashboardRoleAccessState(
|
|
804
|
+
dashboardId,
|
|
805
|
+
userId,
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
if (!hasRequiredRoles) {
|
|
809
|
+
throw new ForbiddenException('Access denied to this dashboard');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
725
813
|
private async getAccessibleTemplateOrThrow(
|
|
726
814
|
userId: number,
|
|
727
815
|
templateSlug: string,
|
|
@@ -1133,6 +1221,8 @@ export class DashboardCoreService {
|
|
|
1133
1221
|
return [];
|
|
1134
1222
|
}
|
|
1135
1223
|
|
|
1224
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1225
|
+
|
|
1136
1226
|
const dashboardItems = await this.prismaService.dashboard_item.findMany({
|
|
1137
1227
|
where: {
|
|
1138
1228
|
dashboard_id: dashboard.id,
|
|
@@ -1208,6 +1298,8 @@ export class DashboardCoreService {
|
|
|
1208
1298
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1209
1299
|
}
|
|
1210
1300
|
|
|
1301
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1302
|
+
|
|
1211
1303
|
const layoutUpdates = layout.flatMap((item) => {
|
|
1212
1304
|
const itemId = Number.parseInt(item.i.replace('widget-', ''), 10);
|
|
1213
1305
|
|
|
@@ -1267,6 +1359,8 @@ export class DashboardCoreService {
|
|
|
1267
1359
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1268
1360
|
}
|
|
1269
1361
|
|
|
1362
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1363
|
+
|
|
1270
1364
|
const userRoles = await this.prismaService.role_user.findMany({
|
|
1271
1365
|
where: { user_id: userId },
|
|
1272
1366
|
select: { role_id: true },
|
|
@@ -1407,6 +1501,8 @@ export class DashboardCoreService {
|
|
|
1407
1501
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1408
1502
|
}
|
|
1409
1503
|
|
|
1504
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1505
|
+
|
|
1410
1506
|
const parsedWidgetId = Number(widgetId.replace(/^widget-/, ''));
|
|
1411
1507
|
|
|
1412
1508
|
if (!Number.isInteger(parsedWidgetId) || parsedWidgetId <= 0) {
|
|
@@ -1464,9 +1560,20 @@ export class DashboardCoreService {
|
|
|
1464
1560
|
},
|
|
1465
1561
|
});
|
|
1466
1562
|
|
|
1467
|
-
|
|
1563
|
+
if (!dashboardUser) {
|
|
1564
|
+
return {
|
|
1565
|
+
hasAccess: false,
|
|
1566
|
+
accessStatus: 'not-shared',
|
|
1567
|
+
dashboard: null,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const roleAccess = await this.getDashboardRoleAccessState(dashboard.id, userId);
|
|
1572
|
+
const hasAccess = roleAccess.hasRequiredRoles;
|
|
1573
|
+
|
|
1468
1574
|
return {
|
|
1469
1575
|
hasAccess,
|
|
1576
|
+
accessStatus: roleAccess.accessStatus,
|
|
1470
1577
|
dashboard: hasAccess
|
|
1471
1578
|
? {
|
|
1472
1579
|
id: dashboard.id,
|
|
@@ -1772,6 +1879,9 @@ export class DashboardCoreService {
|
|
|
1772
1879
|
|
|
1773
1880
|
async getDashboardShares(userId: number, slug: string, locale: string) {
|
|
1774
1881
|
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
1882
|
+
const dashboardRoleRequirements = await this.getDashboardComponentRoleRequirements(
|
|
1883
|
+
dashboardUser.dashboard_id,
|
|
1884
|
+
);
|
|
1775
1885
|
|
|
1776
1886
|
const sharedUsers = await this.prismaService.dashboard_user.findMany({
|
|
1777
1887
|
where: {
|
|
@@ -1782,6 +1892,11 @@ export class DashboardCoreService {
|
|
|
1782
1892
|
select: {
|
|
1783
1893
|
id: true,
|
|
1784
1894
|
name: true,
|
|
1895
|
+
role_user: {
|
|
1896
|
+
select: {
|
|
1897
|
+
role_id: true,
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1785
1900
|
user_identifier: {
|
|
1786
1901
|
where: {
|
|
1787
1902
|
type: 'email',
|
|
@@ -1799,65 +1914,103 @@ export class DashboardCoreService {
|
|
|
1799
1914
|
},
|
|
1800
1915
|
});
|
|
1801
1916
|
|
|
1802
|
-
return sharedUsers.map((sharedDashboardUser) =>
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1917
|
+
return sharedUsers.map((sharedDashboardUser) => {
|
|
1918
|
+
const userRoleIds = sharedDashboardUser.user.role_user.map(
|
|
1919
|
+
(roleUser) => roleUser.role_id,
|
|
1920
|
+
);
|
|
1921
|
+
const hasRequiredRoles = this.userHasRequiredRolesForDashboard(
|
|
1922
|
+
dashboardRoleRequirements,
|
|
1923
|
+
userRoleIds,
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
return {
|
|
1927
|
+
id: sharedDashboardUser.user.id,
|
|
1928
|
+
name: sharedDashboardUser.user.name,
|
|
1929
|
+
email: sharedDashboardUser.user.user_identifier[0]?.value ?? null,
|
|
1930
|
+
isCurrentUser: sharedDashboardUser.user.id === userId,
|
|
1931
|
+
isHome: sharedDashboardUser.is_home,
|
|
1932
|
+
hasRequiredRoles,
|
|
1933
|
+
accessStatus: hasRequiredRoles ? 'allowed' : 'missing-roles',
|
|
1934
|
+
};
|
|
1935
|
+
});
|
|
1809
1936
|
}
|
|
1810
1937
|
|
|
1811
1938
|
async getShareableUsers(
|
|
1812
1939
|
userId: number,
|
|
1813
1940
|
slug: string,
|
|
1814
|
-
|
|
1941
|
+
options:
|
|
1942
|
+
| {
|
|
1943
|
+
search?: string;
|
|
1944
|
+
page?: string | number;
|
|
1945
|
+
pageSize?: string | number;
|
|
1946
|
+
}
|
|
1947
|
+
| undefined,
|
|
1815
1948
|
locale: string,
|
|
1816
1949
|
) {
|
|
1817
1950
|
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
1818
|
-
const normalizedSearch = this.toNullableString(search);
|
|
1951
|
+
const normalizedSearch = this.toNullableString(options?.search);
|
|
1952
|
+
const requestedPage = Number.parseInt(String(options?.page ?? '1'), 10);
|
|
1953
|
+
const requestedPageSize = Number.parseInt(String(options?.pageSize ?? '10'), 10);
|
|
1954
|
+
const page = Number.isFinite(requestedPage) && requestedPage > 0 ? requestedPage : 1;
|
|
1955
|
+
const pageSize = Number.isFinite(requestedPageSize) && requestedPageSize > 0
|
|
1956
|
+
? Math.min(requestedPageSize, 50)
|
|
1957
|
+
: 10;
|
|
1958
|
+
|
|
1959
|
+
const [existingUsers, dashboardRoleRequirements] = await Promise.all([
|
|
1960
|
+
this.prismaService.dashboard_user.findMany({
|
|
1961
|
+
where: {
|
|
1962
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
1963
|
+
},
|
|
1964
|
+
select: {
|
|
1965
|
+
user_id: true,
|
|
1966
|
+
},
|
|
1967
|
+
}),
|
|
1968
|
+
this.getDashboardComponentRoleRequirements(dashboardUser.dashboard_id),
|
|
1969
|
+
]);
|
|
1819
1970
|
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
},
|
|
1824
|
-
select: {
|
|
1825
|
-
user_id: true,
|
|
1971
|
+
const where: any = {
|
|
1972
|
+
id: {
|
|
1973
|
+
notIn: existingUsers.map((item) => item.user_id),
|
|
1826
1974
|
},
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
...(normalizedSearch
|
|
1835
|
-
? {
|
|
1836
|
-
OR: [
|
|
1837
|
-
{
|
|
1838
|
-
name: {
|
|
1839
|
-
contains: normalizedSearch,
|
|
1840
|
-
mode: 'insensitive',
|
|
1841
|
-
},
|
|
1975
|
+
...(normalizedSearch
|
|
1976
|
+
? {
|
|
1977
|
+
OR: [
|
|
1978
|
+
{
|
|
1979
|
+
name: {
|
|
1980
|
+
contains: normalizedSearch,
|
|
1981
|
+
mode: 'insensitive' as const,
|
|
1842
1982
|
},
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
user_identifier: {
|
|
1986
|
+
some: {
|
|
1987
|
+
type: 'email',
|
|
1988
|
+
value: {
|
|
1989
|
+
contains: normalizedSearch,
|
|
1990
|
+
mode: 'insensitive' as const,
|
|
1851
1991
|
},
|
|
1852
1992
|
},
|
|
1853
1993
|
},
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1994
|
+
},
|
|
1995
|
+
],
|
|
1996
|
+
}
|
|
1997
|
+
: {}),
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const total = await this.prismaService.user.count({ where });
|
|
2001
|
+
const lastPage = total > 0 ? Math.ceil(total / pageSize) : 1;
|
|
2002
|
+
const safePage = Math.min(page, lastPage);
|
|
2003
|
+
|
|
2004
|
+
const users = await this.prismaService.user.findMany({
|
|
2005
|
+
where,
|
|
1858
2006
|
select: {
|
|
1859
2007
|
id: true,
|
|
1860
2008
|
name: true,
|
|
2009
|
+
role_user: {
|
|
2010
|
+
select: {
|
|
2011
|
+
role_id: true,
|
|
2012
|
+
},
|
|
2013
|
+
},
|
|
1861
2014
|
user_identifier: {
|
|
1862
2015
|
where: {
|
|
1863
2016
|
type: 'email',
|
|
@@ -1868,34 +2021,66 @@ export class DashboardCoreService {
|
|
|
1868
2021
|
take: 1,
|
|
1869
2022
|
},
|
|
1870
2023
|
},
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
},
|
|
2024
|
+
skip: (safePage - 1) * pageSize,
|
|
2025
|
+
take: pageSize,
|
|
2026
|
+
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
|
1875
2027
|
});
|
|
1876
2028
|
|
|
1877
|
-
return
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2029
|
+
return {
|
|
2030
|
+
data: users.map((candidateUser) => {
|
|
2031
|
+
const userRoleIds = candidateUser.role_user.map(
|
|
2032
|
+
(roleUser) => roleUser.role_id,
|
|
2033
|
+
);
|
|
2034
|
+
const hasRequiredRoles = this.userHasRequiredRolesForDashboard(
|
|
2035
|
+
dashboardRoleRequirements,
|
|
2036
|
+
userRoleIds,
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
return {
|
|
2040
|
+
id: candidateUser.id,
|
|
2041
|
+
name: candidateUser.name,
|
|
2042
|
+
email: candidateUser.user_identifier[0]?.value ?? null,
|
|
2043
|
+
hasRequiredRoles,
|
|
2044
|
+
accessStatus: hasRequiredRoles ? 'allowed' : 'missing-roles',
|
|
2045
|
+
};
|
|
2046
|
+
}),
|
|
2047
|
+
total,
|
|
2048
|
+
page: safePage,
|
|
2049
|
+
pageSize,
|
|
2050
|
+
lastPage,
|
|
2051
|
+
prev: safePage > 1 ? safePage - 1 : null,
|
|
2052
|
+
next: safePage < lastPage ? safePage + 1 : null,
|
|
2053
|
+
};
|
|
1882
2054
|
}
|
|
1883
2055
|
|
|
1884
2056
|
async shareDashboard(
|
|
1885
2057
|
userId: number,
|
|
1886
2058
|
slug: string,
|
|
2059
|
+
sharedUserIds: number[] | undefined,
|
|
1887
2060
|
sharedUserId: number | undefined,
|
|
1888
2061
|
locale: string,
|
|
1889
2062
|
) {
|
|
1890
|
-
|
|
2063
|
+
const requestedIds = Array.from(
|
|
2064
|
+
new Set(
|
|
2065
|
+
[
|
|
2066
|
+
...(Array.isArray(sharedUserIds) ? sharedUserIds : []),
|
|
2067
|
+
...(sharedUserId !== undefined ? [sharedUserId] : []),
|
|
2068
|
+
]
|
|
2069
|
+
.map((value) => Number(value))
|
|
2070
|
+
.filter((value) => Number.isInteger(value) && value > 0),
|
|
2071
|
+
),
|
|
2072
|
+
);
|
|
2073
|
+
|
|
2074
|
+
if (requestedIds.length === 0) {
|
|
1891
2075
|
throw new BadRequestException(
|
|
1892
2076
|
getLocaleText('validation.fieldRequired', locale, 'User is required.'),
|
|
1893
2077
|
);
|
|
1894
2078
|
}
|
|
1895
2079
|
|
|
1896
2080
|
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
2081
|
+
const sanitizedUserIds = requestedIds.filter((candidateUserId) => candidateUserId !== userId);
|
|
1897
2082
|
|
|
1898
|
-
if (
|
|
2083
|
+
if (sanitizedUserIds.length === 0) {
|
|
1899
2084
|
throw new BadRequestException(
|
|
1900
2085
|
getLocaleText(
|
|
1901
2086
|
'validation.invalidValue',
|
|
@@ -1905,40 +2090,56 @@ export class DashboardCoreService {
|
|
|
1905
2090
|
);
|
|
1906
2091
|
}
|
|
1907
2092
|
|
|
1908
|
-
const
|
|
1909
|
-
where: {
|
|
1910
|
-
|
|
2093
|
+
const targetUsers = await this.prismaService.user.findMany({
|
|
2094
|
+
where: {
|
|
2095
|
+
id: {
|
|
2096
|
+
in: sanitizedUserIds,
|
|
2097
|
+
},
|
|
2098
|
+
},
|
|
2099
|
+
select: {
|
|
2100
|
+
id: true,
|
|
2101
|
+
},
|
|
1911
2102
|
});
|
|
1912
2103
|
|
|
1913
|
-
if (
|
|
1914
|
-
throw new BadRequestException(
|
|
2104
|
+
if (targetUsers.length !== sanitizedUserIds.length) {
|
|
2105
|
+
throw new BadRequestException(
|
|
2106
|
+
getLocaleText('userNotFound', locale, 'User not found.'),
|
|
2107
|
+
);
|
|
1915
2108
|
}
|
|
1916
2109
|
|
|
1917
|
-
const
|
|
2110
|
+
const existingShares = await this.prismaService.dashboard_user.findMany({
|
|
1918
2111
|
where: {
|
|
1919
2112
|
dashboard_id: dashboardUser.dashboard_id,
|
|
1920
|
-
user_id:
|
|
2113
|
+
user_id: {
|
|
2114
|
+
in: sanitizedUserIds,
|
|
2115
|
+
},
|
|
2116
|
+
},
|
|
2117
|
+
select: {
|
|
2118
|
+
user_id: true,
|
|
1921
2119
|
},
|
|
1922
|
-
select: { id: true },
|
|
1923
2120
|
});
|
|
1924
2121
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
}
|
|
2122
|
+
const alreadySharedUserIds = existingShares.map((item) => item.user_id);
|
|
2123
|
+
const alreadySharedSet = new Set(alreadySharedUserIds);
|
|
2124
|
+
const newUserIds = sanitizedUserIds.filter(
|
|
2125
|
+
(candidateUserId) => !alreadySharedSet.has(candidateUserId),
|
|
2126
|
+
);
|
|
1931
2127
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2128
|
+
if (newUserIds.length > 0) {
|
|
2129
|
+
await this.prismaService.dashboard_user.createMany({
|
|
2130
|
+
data: newUserIds.map((candidateUserId) => ({
|
|
2131
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
2132
|
+
user_id: candidateUserId,
|
|
2133
|
+
is_home: false,
|
|
2134
|
+
})),
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
1939
2137
|
|
|
1940
2138
|
return {
|
|
1941
2139
|
success: true,
|
|
2140
|
+
sharedCount: newUserIds.length,
|
|
2141
|
+
sharedUserIds: newUserIds,
|
|
2142
|
+
alreadySharedUserIds,
|
|
1942
2143
|
};
|
|
1943
2144
|
}
|
|
1944
2145
|
|
package/src/index.ts
CHANGED
|
@@ -19,12 +19,13 @@ export * from './screen/screen.service';
|
|
|
19
19
|
export * from './user/constants/user.constants';
|
|
20
20
|
export * from './user/user.service';
|
|
21
21
|
|
|
22
|
-
export * from './mail/mail.module';
|
|
23
|
-
export * from './mail/mail.service';
|
|
24
|
-
|
|
25
|
-
export * from './file/file.module';
|
|
26
|
-
export * from './file/file.service';
|
|
27
|
-
export * from './setting/setting.
|
|
22
|
+
export * from './mail/mail.module';
|
|
23
|
+
export * from './mail/mail.service';
|
|
24
|
+
|
|
25
|
+
export * from './file/file.module';
|
|
26
|
+
export * from './file/file.service';
|
|
27
|
+
export * from './setting/setting.module';
|
|
28
|
+
export * from './setting/setting.service';
|
|
28
29
|
|
|
29
30
|
export * from './ai/ai.module';
|
|
30
31
|
export * from './ai/ai.service';
|