@hed-hog/core 0.0.300 → 0.0.301

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 (35) hide show
  1. package/dist/ai/ai.service.d.ts +13 -2
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +104 -2
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +26 -9
  6. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +11 -5
  8. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +34 -10
  10. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-core/dashboard-core.service.js +196 -69
  12. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/mail/mail.service.d.ts +9 -2
  18. package/dist/mail/mail.service.d.ts.map +1 -1
  19. package/dist/mail/mail.service.js +56 -4
  20. package/dist/mail/mail.service.js.map +1 -1
  21. package/dist/setting/setting.service.d.ts +6 -1
  22. package/dist/setting/setting.service.d.ts.map +1 -1
  23. package/dist/setting/setting.service.js +188 -15
  24. package/dist/setting/setting.service.js.map +1 -1
  25. package/hedhog/data/setting_group.yaml +28 -0
  26. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +305 -75
  27. package/hedhog/frontend/messages/en.json +15 -3
  28. package/hedhog/frontend/messages/pt.json +15 -3
  29. package/package.json +5 -5
  30. package/src/ai/ai.service.ts +129 -1
  31. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +9 -2
  32. package/src/dashboard/dashboard-core/dashboard-core.service.ts +276 -75
  33. package/src/index.ts +7 -6
  34. package/src/mail/mail.service.ts +67 -3
  35. package/src/setting/setting.service.ts +222 -15
@@ -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
- search,
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
- const hasAccess = Boolean(dashboardUser);
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
- id: sharedDashboardUser.user.id,
1804
- name: sharedDashboardUser.user.name,
1805
- email: sharedDashboardUser.user.user_identifier[0]?.value ?? null,
1806
- isCurrentUser: sharedDashboardUser.user.id === userId,
1807
- isHome: sharedDashboardUser.is_home,
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
- search: string | undefined,
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 existingUsers = await this.prismaService.dashboard_user.findMany({
1821
- where: {
1822
- dashboard_id: dashboardUser.dashboard_id,
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
- const users = await this.prismaService.user.findMany({
1830
- where: {
1831
- id: {
1832
- notIn: existingUsers.map((item) => item.user_id),
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
- user_identifier: {
1845
- some: {
1846
- type: 'email',
1847
- value: {
1848
- contains: normalizedSearch,
1849
- mode: 'insensitive',
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
- take: 20,
1872
- orderBy: {
1873
- id: 'desc',
1874
- },
2024
+ skip: (safePage - 1) * pageSize,
2025
+ take: pageSize,
2026
+ orderBy: [{ name: 'asc' }, { id: 'asc' }],
1875
2027
  });
1876
2028
 
1877
- return users.map((candidateUser) => ({
1878
- id: candidateUser.id,
1879
- name: candidateUser.name,
1880
- email: candidateUser.user_identifier[0]?.value ?? null,
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
- if (!sharedUserId || Number.isNaN(Number(sharedUserId))) {
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 (sharedUserId === userId) {
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 targetUser = await this.prismaService.user.findUnique({
1909
- where: { id: sharedUserId },
1910
- select: { id: true },
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 (!targetUser) {
1914
- throw new BadRequestException(getLocaleText('userNotFound', locale, 'User not found.'));
2104
+ if (targetUsers.length !== sanitizedUserIds.length) {
2105
+ throw new BadRequestException(
2106
+ getLocaleText('userNotFound', locale, 'User not found.'),
2107
+ );
1915
2108
  }
1916
2109
 
1917
- const existingShare = await this.prismaService.dashboard_user.findFirst({
2110
+ const existingShares = await this.prismaService.dashboard_user.findMany({
1918
2111
  where: {
1919
2112
  dashboard_id: dashboardUser.dashboard_id,
1920
- user_id: sharedUserId,
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
- if (existingShare) {
1926
- return {
1927
- success: true,
1928
- alreadyShared: true,
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
- await this.prismaService.dashboard_user.create({
1933
- data: {
1934
- dashboard_id: dashboardUser.dashboard_id,
1935
- user_id: sharedUserId,
1936
- is_home: false,
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.service';
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';