@hed-hog/core 0.0.299 → 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.
- 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/dashboard.controller.d.ts +6 -0
- package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -0
- package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +2 -1
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.controller.js +6 -3
- package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +7 -1
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-component/dashboard-component.service.js +76 -33
- package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +82 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +117 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +93 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +654 -20
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -0
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -0
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.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/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/dashboard.yaml +12 -6
- package/hedhog/data/dashboard_component_role.yaml +66 -0
- package/hedhog/data/dashboard_role.yaml +2 -8
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/data/setting_group.yaml +28 -0
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +333 -128
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +277 -53
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +179 -231
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1619 -0
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
- package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
- package/hedhog/frontend/messages/en.json +124 -2
- package/hedhog/frontend/messages/pt.json +123 -1
- package/hedhog/frontend/widgets/account-security.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/active-users-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/email-notifications.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/locale-config.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/mail-config.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/menus-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/oauth-config.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/permissions-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/profile-card.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/routes-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/storage-config.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/theme-config.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/user-roles.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/user-sessions.tsx.ejs +1 -1
- package/hedhog/table/dashboard.yaml +6 -0
- package/package.json +3 -3
- package/src/ai/ai.service.ts +129 -1
- package/src/dashboard/dashboard-component/dashboard-component.controller.ts +15 -2
- package/src/dashboard/dashboard-component/dashboard-component.service.ts +107 -43
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +119 -1
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +876 -20
- package/src/index.ts +7 -6
- package/src/mail/mail.service.ts +67 -3
- package/src/setting/setting.service.ts +222 -15
- package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +0 -11
- package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +0 -192
- package/hedhog/frontend/app/dashboard/components/widgets/core.active-users-card.tsx.ejs +0 -58
- package/hedhog/frontend/app/dashboard/components/widgets/core.activity-timeline.tsx.ejs +0 -223
- package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +0 -226
- package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +0 -168
- package/hedhog/frontend/app/dashboard/components/widgets/core.login-history-chart.tsx.ejs +0 -115
- package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +0 -199
- package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-card.tsx.ejs +0 -58
- package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-chart.tsx.ejs +0 -149
- package/hedhog/frontend/app/dashboard/components/widgets/core.menus-card.tsx.ejs +0 -58
- package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +0 -175
- package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-card.tsx.ejs +0 -61
- package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-chart.tsx.ejs +0 -156
- package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +0 -186
- package/hedhog/frontend/app/dashboard/components/widgets/core.routes-card.tsx.ejs +0 -58
- package/hedhog/frontend/app/dashboard/components/widgets/core.session-activity-chart.tsx.ejs +0 -183
- package/hedhog/frontend/app/dashboard/components/widgets/core.sessions-today-card.tsx.ejs +0 -62
- package/hedhog/frontend/app/dashboard/components/widgets/core.stat-access-level.tsx.ejs +0 -57
- package/hedhog/frontend/app/dashboard/components/widgets/core.stat-actions-today.tsx.ejs +0 -57
- package/hedhog/frontend/app/dashboard/components/widgets/core.stat-consecutive-days.tsx.ejs +0 -57
- package/hedhog/frontend/app/dashboard/components/widgets/core.stat-online-time.tsx.ejs +0 -57
- package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +0 -196
- package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +0 -213
- package/hedhog/frontend/app/dashboard/components/widgets/core.user-growth-chart.tsx.ejs +0 -210
- package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +0 -132
- package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +0 -236
- package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +0 -108
- package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +0 -66
- package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +0 -122
- package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +0 -63
- package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +0 -73
- package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +0 -73
- package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +0 -123
- package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +0 -118
|
@@ -642,6 +642,241 @@ export class DashboardCoreService {
|
|
|
642
642
|
return value.map((provider) => String(provider).toLowerCase());
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
+
private slugifyDashboardName(value: string): string {
|
|
646
|
+
const normalized = value
|
|
647
|
+
.normalize('NFD')
|
|
648
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
649
|
+
.toLowerCase()
|
|
650
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
651
|
+
.replace(/^-+|-+$/g, '');
|
|
652
|
+
|
|
653
|
+
return normalized || `dashboard-${Date.now()}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private getDashboardDisplayName(
|
|
657
|
+
dashboard: {
|
|
658
|
+
slug: string;
|
|
659
|
+
dashboard_locale?: Array<{ name?: string | null }>;
|
|
660
|
+
},
|
|
661
|
+
): string {
|
|
662
|
+
return dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private async buildUniqueDashboardSlug(baseSlug: string): Promise<string> {
|
|
666
|
+
let slug = baseSlug;
|
|
667
|
+
let suffix = 2;
|
|
668
|
+
|
|
669
|
+
while (
|
|
670
|
+
await this.prismaService.dashboard.findFirst({
|
|
671
|
+
where: { slug },
|
|
672
|
+
select: { id: true },
|
|
673
|
+
})
|
|
674
|
+
) {
|
|
675
|
+
slug = `${baseSlug}-${suffix}`;
|
|
676
|
+
suffix += 1;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return slug;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private async getDashboardUserOrThrow(
|
|
683
|
+
userId: number,
|
|
684
|
+
slug: string,
|
|
685
|
+
locale: string,
|
|
686
|
+
) {
|
|
687
|
+
const dashboardUser = await this.prismaService.dashboard_user.findFirst({
|
|
688
|
+
where: {
|
|
689
|
+
user_id: userId,
|
|
690
|
+
dashboard: { slug },
|
|
691
|
+
},
|
|
692
|
+
include: {
|
|
693
|
+
dashboard: {
|
|
694
|
+
include: {
|
|
695
|
+
dashboard_locale: {
|
|
696
|
+
where: {
|
|
697
|
+
locale: {
|
|
698
|
+
code: locale,
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (!dashboardUser) {
|
|
708
|
+
throw new ForbiddenException(
|
|
709
|
+
getLocaleText('dashboardNotFound', locale, 'Dashboard not found.'),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return dashboardUser;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private async getUserRoleIds(userId: number) {
|
|
717
|
+
const roleUsers = await this.prismaService.role_user.findMany({
|
|
718
|
+
where: { user_id: userId },
|
|
719
|
+
select: { role_id: true },
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
return roleUsers.map((roleUser) => roleUser.role_id);
|
|
723
|
+
}
|
|
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
|
+
|
|
813
|
+
private async getAccessibleTemplateOrThrow(
|
|
814
|
+
userId: number,
|
|
815
|
+
templateSlug: string,
|
|
816
|
+
locale: string,
|
|
817
|
+
) {
|
|
818
|
+
const userRoleIds = await this.getUserRoleIds(userId);
|
|
819
|
+
const templateAccessFilter =
|
|
820
|
+
userRoleIds.length > 0
|
|
821
|
+
? {
|
|
822
|
+
OR: [
|
|
823
|
+
{
|
|
824
|
+
dashboard_role: {
|
|
825
|
+
some: {
|
|
826
|
+
role_id: {
|
|
827
|
+
in: userRoleIds,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
dashboard_role: {
|
|
834
|
+
none: {},
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
}
|
|
839
|
+
: {
|
|
840
|
+
dashboard_role: {
|
|
841
|
+
none: {},
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const template = await this.prismaService.dashboard.findFirst({
|
|
846
|
+
where: {
|
|
847
|
+
slug: templateSlug,
|
|
848
|
+
is_template: true,
|
|
849
|
+
...templateAccessFilter,
|
|
850
|
+
},
|
|
851
|
+
include: {
|
|
852
|
+
dashboard_locale: {
|
|
853
|
+
where: {
|
|
854
|
+
locale: {
|
|
855
|
+
code: locale,
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
dashboard_item: {
|
|
860
|
+
select: {
|
|
861
|
+
component_id: true,
|
|
862
|
+
width: true,
|
|
863
|
+
height: true,
|
|
864
|
+
x_axis: true,
|
|
865
|
+
y_axis: true,
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
if (!template) {
|
|
872
|
+
throw new ForbiddenException(
|
|
873
|
+
getLocaleText('dashboardNotFound', locale, 'Dashboard template not found.'),
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return template;
|
|
878
|
+
}
|
|
879
|
+
|
|
645
880
|
async getHome(userId: number, locale: string) {
|
|
646
881
|
const user = await this.prismaService.user.findUnique({
|
|
647
882
|
where: { id: userId },
|
|
@@ -986,6 +1221,8 @@ export class DashboardCoreService {
|
|
|
986
1221
|
return [];
|
|
987
1222
|
}
|
|
988
1223
|
|
|
1224
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1225
|
+
|
|
989
1226
|
const dashboardItems = await this.prismaService.dashboard_item.findMany({
|
|
990
1227
|
where: {
|
|
991
1228
|
dashboard_id: dashboard.id,
|
|
@@ -1061,28 +1298,36 @@ export class DashboardCoreService {
|
|
|
1061
1298
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1062
1299
|
}
|
|
1063
1300
|
|
|
1064
|
-
|
|
1065
|
-
const itemId = parseInt(item.i.replace('widget-', ''));
|
|
1066
|
-
const dashboardItem = await this.prismaService.dashboard_item.findFirst({
|
|
1067
|
-
where: {
|
|
1068
|
-
id: itemId,
|
|
1069
|
-
dashboard_id: dashboard.id,
|
|
1070
|
-
},
|
|
1071
|
-
});
|
|
1301
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1072
1302
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1303
|
+
const layoutUpdates = layout.flatMap((item) => {
|
|
1304
|
+
const itemId = Number.parseInt(item.i.replace('widget-', ''), 10);
|
|
1305
|
+
|
|
1306
|
+
if (Number.isNaN(itemId)) {
|
|
1307
|
+
this.logger.warn(
|
|
1308
|
+
`Skipping dashboard layout item with invalid id: ${item.i}`,
|
|
1309
|
+
);
|
|
1310
|
+
return [];
|
|
1075
1311
|
}
|
|
1076
1312
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1313
|
+
return [
|
|
1314
|
+
this.prismaService.dashboard_item.updateMany({
|
|
1315
|
+
where: {
|
|
1316
|
+
id: itemId,
|
|
1317
|
+
dashboard_id: dashboard.id,
|
|
1318
|
+
},
|
|
1319
|
+
data: {
|
|
1320
|
+
x_axis: item.x,
|
|
1321
|
+
y_axis: item.y,
|
|
1322
|
+
width: item.w,
|
|
1323
|
+
height: item.h,
|
|
1324
|
+
},
|
|
1325
|
+
}),
|
|
1326
|
+
];
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
if (layoutUpdates.length > 0) {
|
|
1330
|
+
await this.prismaService.$transaction(layoutUpdates);
|
|
1086
1331
|
}
|
|
1087
1332
|
|
|
1088
1333
|
return { success: true };
|
|
@@ -1114,6 +1359,8 @@ export class DashboardCoreService {
|
|
|
1114
1359
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1115
1360
|
}
|
|
1116
1361
|
|
|
1362
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1363
|
+
|
|
1117
1364
|
const userRoles = await this.prismaService.role_user.findMany({
|
|
1118
1365
|
where: { user_id: userId },
|
|
1119
1366
|
select: { role_id: true },
|
|
@@ -1254,6 +1501,8 @@ export class DashboardCoreService {
|
|
|
1254
1501
|
throw new ForbiddenException('Access denied to this dashboard');
|
|
1255
1502
|
}
|
|
1256
1503
|
|
|
1504
|
+
await this.assertDashboardRoleAccess(dashboard.id, userId);
|
|
1505
|
+
|
|
1257
1506
|
const parsedWidgetId = Number(widgetId.replace(/^widget-/, ''));
|
|
1258
1507
|
|
|
1259
1508
|
if (!Number.isInteger(parsedWidgetId) || parsedWidgetId <= 0) {
|
|
@@ -1311,9 +1560,20 @@ export class DashboardCoreService {
|
|
|
1311
1560
|
},
|
|
1312
1561
|
});
|
|
1313
1562
|
|
|
1314
|
-
|
|
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
|
+
|
|
1315
1574
|
return {
|
|
1316
1575
|
hasAccess,
|
|
1576
|
+
accessStatus: roleAccess.accessStatus,
|
|
1317
1577
|
dashboard: hasAccess
|
|
1318
1578
|
? {
|
|
1319
1579
|
id: dashboard.id,
|
|
@@ -1325,6 +1585,8 @@ export class DashboardCoreService {
|
|
|
1325
1585
|
}
|
|
1326
1586
|
|
|
1327
1587
|
async getUserDashboards(userId: number, locale: string) {
|
|
1588
|
+
await this.getHome(userId, locale);
|
|
1589
|
+
|
|
1328
1590
|
const dashboardUsers = await this.prismaService.dashboard_user.findMany({
|
|
1329
1591
|
where: { user_id: userId },
|
|
1330
1592
|
include: {
|
|
@@ -1340,6 +1602,7 @@ export class DashboardCoreService {
|
|
|
1340
1602
|
},
|
|
1341
1603
|
},
|
|
1342
1604
|
},
|
|
1605
|
+
orderBy: [{ is_home: 'desc' }, { id: 'asc' }],
|
|
1343
1606
|
});
|
|
1344
1607
|
|
|
1345
1608
|
const uniqueByDashboardId = new Map<number, (typeof dashboardUsers)[number]>();
|
|
@@ -1352,10 +1615,603 @@ export class DashboardCoreService {
|
|
|
1352
1615
|
return Array.from(uniqueByDashboardId.values()).map((dashboardUser) => ({
|
|
1353
1616
|
id: dashboardUser.dashboard.id,
|
|
1354
1617
|
slug: dashboardUser.dashboard.slug,
|
|
1618
|
+
name: this.getDashboardDisplayName(dashboardUser.dashboard),
|
|
1619
|
+
icon: dashboardUser.dashboard.icon,
|
|
1620
|
+
is_home: dashboardUser.is_home,
|
|
1355
1621
|
dashboard_locale: dashboardUser.dashboard.dashboard_locale,
|
|
1356
1622
|
}));
|
|
1357
1623
|
}
|
|
1358
1624
|
|
|
1625
|
+
async getAvailableTemplates(userId: number, locale: string) {
|
|
1626
|
+
const userRoleIds = await this.getUserRoleIds(userId);
|
|
1627
|
+
const templateAccessFilter =
|
|
1628
|
+
userRoleIds.length > 0
|
|
1629
|
+
? {
|
|
1630
|
+
OR: [
|
|
1631
|
+
{
|
|
1632
|
+
dashboard_role: {
|
|
1633
|
+
some: {
|
|
1634
|
+
role_id: {
|
|
1635
|
+
in: userRoleIds,
|
|
1636
|
+
},
|
|
1637
|
+
},
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
dashboard_role: {
|
|
1642
|
+
none: {},
|
|
1643
|
+
},
|
|
1644
|
+
},
|
|
1645
|
+
],
|
|
1646
|
+
}
|
|
1647
|
+
: {
|
|
1648
|
+
dashboard_role: {
|
|
1649
|
+
none: {},
|
|
1650
|
+
},
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
const templates = await this.prismaService.dashboard.findMany({
|
|
1654
|
+
where: {
|
|
1655
|
+
is_template: true,
|
|
1656
|
+
...templateAccessFilter,
|
|
1657
|
+
},
|
|
1658
|
+
include: {
|
|
1659
|
+
dashboard_locale: {
|
|
1660
|
+
where: {
|
|
1661
|
+
locale: {
|
|
1662
|
+
code: locale,
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
},
|
|
1666
|
+
dashboard_item: {
|
|
1667
|
+
select: { id: true },
|
|
1668
|
+
},
|
|
1669
|
+
},
|
|
1670
|
+
orderBy: [{ id: 'asc' }],
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
return templates.map((template) => ({
|
|
1674
|
+
id: template.id,
|
|
1675
|
+
slug: template.slug,
|
|
1676
|
+
name: this.getDashboardDisplayName(template),
|
|
1677
|
+
icon: template.icon,
|
|
1678
|
+
itemCount: template.dashboard_item.length,
|
|
1679
|
+
}));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
async createUserDashboard(
|
|
1683
|
+
userId: number,
|
|
1684
|
+
data: { name?: string; slug?: string; icon?: string | null; templateSlug?: string },
|
|
1685
|
+
locale: string,
|
|
1686
|
+
) {
|
|
1687
|
+
const templateSlug = this.toNullableString(data?.templateSlug);
|
|
1688
|
+
const template = templateSlug
|
|
1689
|
+
? await this.getAccessibleTemplateOrThrow(userId, templateSlug, locale)
|
|
1690
|
+
: null;
|
|
1691
|
+
|
|
1692
|
+
const templateName = template ? this.getDashboardDisplayName(template) : null;
|
|
1693
|
+
const name = this.toNullableString(data?.name) || templateName;
|
|
1694
|
+
|
|
1695
|
+
if (!name) {
|
|
1696
|
+
throw new BadRequestException(
|
|
1697
|
+
getLocaleText('validation.fieldRequired', locale, 'Dashboard name is required.'),
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const requestedSlug = this.toNullableString(data?.slug);
|
|
1702
|
+
const baseSlug = this.slugifyDashboardName(requestedSlug || name);
|
|
1703
|
+
const slug = await this.buildUniqueDashboardSlug(baseSlug);
|
|
1704
|
+
const icon =
|
|
1705
|
+
data?.icon === undefined
|
|
1706
|
+
? template?.icon ?? null
|
|
1707
|
+
: this.toNullableString(data?.icon) ?? template?.icon ?? null;
|
|
1708
|
+
|
|
1709
|
+
const [localeRecord, existingCount] = await Promise.all([
|
|
1710
|
+
this.prismaService.locale.findFirst({
|
|
1711
|
+
where: { code: locale },
|
|
1712
|
+
select: { id: true },
|
|
1713
|
+
}),
|
|
1714
|
+
this.prismaService.dashboard_user.count({
|
|
1715
|
+
where: { user_id: userId },
|
|
1716
|
+
}),
|
|
1717
|
+
]);
|
|
1718
|
+
|
|
1719
|
+
const dashboard = await this.prismaService.dashboard.create({
|
|
1720
|
+
data: {
|
|
1721
|
+
slug,
|
|
1722
|
+
icon,
|
|
1723
|
+
},
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
if (localeRecord) {
|
|
1727
|
+
await this.prismaService.dashboard_locale.create({
|
|
1728
|
+
data: {
|
|
1729
|
+
dashboard_id: dashboard.id,
|
|
1730
|
+
locale_id: localeRecord.id,
|
|
1731
|
+
name,
|
|
1732
|
+
},
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
await this.prismaService.dashboard_user.create({
|
|
1737
|
+
data: {
|
|
1738
|
+
dashboard_id: dashboard.id,
|
|
1739
|
+
user_id: userId,
|
|
1740
|
+
is_home: existingCount === 0,
|
|
1741
|
+
},
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
if (template?.dashboard_item?.length) {
|
|
1745
|
+
await this.prismaService.dashboard_item.createMany({
|
|
1746
|
+
data: template.dashboard_item.map((item) => ({
|
|
1747
|
+
dashboard_id: dashboard.id,
|
|
1748
|
+
component_id: item.component_id,
|
|
1749
|
+
width: item.width,
|
|
1750
|
+
height: item.height,
|
|
1751
|
+
x_axis: item.x_axis,
|
|
1752
|
+
y_axis: item.y_axis,
|
|
1753
|
+
})),
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return {
|
|
1758
|
+
id: dashboard.id,
|
|
1759
|
+
slug,
|
|
1760
|
+
name,
|
|
1761
|
+
icon,
|
|
1762
|
+
is_home: existingCount === 0,
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
async renameUserDashboard(
|
|
1767
|
+
userId: number,
|
|
1768
|
+
slug: string,
|
|
1769
|
+
data: { name?: string; icon?: string | null },
|
|
1770
|
+
locale: string,
|
|
1771
|
+
) {
|
|
1772
|
+
const dashboardUser = await this.getDashboardUserOrThrow(
|
|
1773
|
+
userId,
|
|
1774
|
+
slug,
|
|
1775
|
+
locale,
|
|
1776
|
+
);
|
|
1777
|
+
const name = this.toNullableString(data?.name);
|
|
1778
|
+
const normalizedIcon =
|
|
1779
|
+
data?.icon === undefined ? undefined : this.toNullableString(data?.icon);
|
|
1780
|
+
|
|
1781
|
+
if (!name && data?.icon === undefined) {
|
|
1782
|
+
throw new BadRequestException(
|
|
1783
|
+
getLocaleText(
|
|
1784
|
+
'validation.fieldRequired',
|
|
1785
|
+
locale,
|
|
1786
|
+
'Dashboard name or icon is required.',
|
|
1787
|
+
),
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (data?.icon !== undefined) {
|
|
1792
|
+
await this.prismaService.dashboard.update({
|
|
1793
|
+
where: { id: dashboardUser.dashboard_id },
|
|
1794
|
+
data: {
|
|
1795
|
+
icon: normalizedIcon ?? null,
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (name) {
|
|
1801
|
+
const localeRecord = await this.prismaService.locale.findFirst({
|
|
1802
|
+
where: { code: locale },
|
|
1803
|
+
select: { id: true },
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
if (!localeRecord) {
|
|
1807
|
+
throw new BadRequestException(
|
|
1808
|
+
getLocaleText('localeNotFound', locale, 'Locale not found.'),
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const existingLocale = await this.prismaService.dashboard_locale.findFirst({
|
|
1813
|
+
where: {
|
|
1814
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
1815
|
+
locale_id: localeRecord.id,
|
|
1816
|
+
},
|
|
1817
|
+
select: { id: true },
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
if (existingLocale) {
|
|
1821
|
+
await this.prismaService.dashboard_locale.update({
|
|
1822
|
+
where: { id: existingLocale.id },
|
|
1823
|
+
data: { name },
|
|
1824
|
+
});
|
|
1825
|
+
} else {
|
|
1826
|
+
await this.prismaService.dashboard_locale.create({
|
|
1827
|
+
data: {
|
|
1828
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
1829
|
+
locale_id: localeRecord.id,
|
|
1830
|
+
name,
|
|
1831
|
+
},
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const updatedDashboard = await this.prismaService.dashboard.findUnique({
|
|
1837
|
+
where: { id: dashboardUser.dashboard_id },
|
|
1838
|
+
include: {
|
|
1839
|
+
dashboard_locale: {
|
|
1840
|
+
where: {
|
|
1841
|
+
locale: {
|
|
1842
|
+
code: locale,
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
},
|
|
1846
|
+
},
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
return {
|
|
1850
|
+
success: true,
|
|
1851
|
+
slug,
|
|
1852
|
+
name:
|
|
1853
|
+
name ||
|
|
1854
|
+
(updatedDashboard
|
|
1855
|
+
? this.getDashboardDisplayName(updatedDashboard)
|
|
1856
|
+
: slug),
|
|
1857
|
+
icon: updatedDashboard?.icon ?? normalizedIcon ?? null,
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
async setHomeDashboard(userId: number, slug: string, locale: string) {
|
|
1862
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
1863
|
+
|
|
1864
|
+
await this.prismaService.dashboard_user.updateMany({
|
|
1865
|
+
where: { user_id: userId },
|
|
1866
|
+
data: { is_home: false },
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
await this.prismaService.dashboard_user.update({
|
|
1870
|
+
where: { id: dashboardUser.id },
|
|
1871
|
+
data: { is_home: true },
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
return {
|
|
1875
|
+
success: true,
|
|
1876
|
+
slug,
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
async getDashboardShares(userId: number, slug: string, locale: string) {
|
|
1881
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
1882
|
+
const dashboardRoleRequirements = await this.getDashboardComponentRoleRequirements(
|
|
1883
|
+
dashboardUser.dashboard_id,
|
|
1884
|
+
);
|
|
1885
|
+
|
|
1886
|
+
const sharedUsers = await this.prismaService.dashboard_user.findMany({
|
|
1887
|
+
where: {
|
|
1888
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
1889
|
+
},
|
|
1890
|
+
include: {
|
|
1891
|
+
user: {
|
|
1892
|
+
select: {
|
|
1893
|
+
id: true,
|
|
1894
|
+
name: true,
|
|
1895
|
+
role_user: {
|
|
1896
|
+
select: {
|
|
1897
|
+
role_id: true,
|
|
1898
|
+
},
|
|
1899
|
+
},
|
|
1900
|
+
user_identifier: {
|
|
1901
|
+
where: {
|
|
1902
|
+
type: 'email',
|
|
1903
|
+
},
|
|
1904
|
+
select: {
|
|
1905
|
+
value: true,
|
|
1906
|
+
},
|
|
1907
|
+
take: 1,
|
|
1908
|
+
},
|
|
1909
|
+
},
|
|
1910
|
+
},
|
|
1911
|
+
},
|
|
1912
|
+
orderBy: {
|
|
1913
|
+
id: 'asc',
|
|
1914
|
+
},
|
|
1915
|
+
});
|
|
1916
|
+
|
|
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
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
async getShareableUsers(
|
|
1939
|
+
userId: number,
|
|
1940
|
+
slug: string,
|
|
1941
|
+
options:
|
|
1942
|
+
| {
|
|
1943
|
+
search?: string;
|
|
1944
|
+
page?: string | number;
|
|
1945
|
+
pageSize?: string | number;
|
|
1946
|
+
}
|
|
1947
|
+
| undefined,
|
|
1948
|
+
locale: string,
|
|
1949
|
+
) {
|
|
1950
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
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
|
+
]);
|
|
1970
|
+
|
|
1971
|
+
const where: any = {
|
|
1972
|
+
id: {
|
|
1973
|
+
notIn: existingUsers.map((item) => item.user_id),
|
|
1974
|
+
},
|
|
1975
|
+
...(normalizedSearch
|
|
1976
|
+
? {
|
|
1977
|
+
OR: [
|
|
1978
|
+
{
|
|
1979
|
+
name: {
|
|
1980
|
+
contains: normalizedSearch,
|
|
1981
|
+
mode: 'insensitive' as const,
|
|
1982
|
+
},
|
|
1983
|
+
},
|
|
1984
|
+
{
|
|
1985
|
+
user_identifier: {
|
|
1986
|
+
some: {
|
|
1987
|
+
type: 'email',
|
|
1988
|
+
value: {
|
|
1989
|
+
contains: normalizedSearch,
|
|
1990
|
+
mode: 'insensitive' as const,
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
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,
|
|
2006
|
+
select: {
|
|
2007
|
+
id: true,
|
|
2008
|
+
name: true,
|
|
2009
|
+
role_user: {
|
|
2010
|
+
select: {
|
|
2011
|
+
role_id: true,
|
|
2012
|
+
},
|
|
2013
|
+
},
|
|
2014
|
+
user_identifier: {
|
|
2015
|
+
where: {
|
|
2016
|
+
type: 'email',
|
|
2017
|
+
},
|
|
2018
|
+
select: {
|
|
2019
|
+
value: true,
|
|
2020
|
+
},
|
|
2021
|
+
take: 1,
|
|
2022
|
+
},
|
|
2023
|
+
},
|
|
2024
|
+
skip: (safePage - 1) * pageSize,
|
|
2025
|
+
take: pageSize,
|
|
2026
|
+
orderBy: [{ name: 'asc' }, { id: 'asc' }],
|
|
2027
|
+
});
|
|
2028
|
+
|
|
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
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
async shareDashboard(
|
|
2057
|
+
userId: number,
|
|
2058
|
+
slug: string,
|
|
2059
|
+
sharedUserIds: number[] | undefined,
|
|
2060
|
+
sharedUserId: number | undefined,
|
|
2061
|
+
locale: string,
|
|
2062
|
+
) {
|
|
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) {
|
|
2075
|
+
throw new BadRequestException(
|
|
2076
|
+
getLocaleText('validation.fieldRequired', locale, 'User is required.'),
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
2081
|
+
const sanitizedUserIds = requestedIds.filter((candidateUserId) => candidateUserId !== userId);
|
|
2082
|
+
|
|
2083
|
+
if (sanitizedUserIds.length === 0) {
|
|
2084
|
+
throw new BadRequestException(
|
|
2085
|
+
getLocaleText(
|
|
2086
|
+
'validation.invalidValue',
|
|
2087
|
+
locale,
|
|
2088
|
+
'You already have access to this dashboard.',
|
|
2089
|
+
),
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const targetUsers = await this.prismaService.user.findMany({
|
|
2094
|
+
where: {
|
|
2095
|
+
id: {
|
|
2096
|
+
in: sanitizedUserIds,
|
|
2097
|
+
},
|
|
2098
|
+
},
|
|
2099
|
+
select: {
|
|
2100
|
+
id: true,
|
|
2101
|
+
},
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
if (targetUsers.length !== sanitizedUserIds.length) {
|
|
2105
|
+
throw new BadRequestException(
|
|
2106
|
+
getLocaleText('userNotFound', locale, 'User not found.'),
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const existingShares = await this.prismaService.dashboard_user.findMany({
|
|
2111
|
+
where: {
|
|
2112
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
2113
|
+
user_id: {
|
|
2114
|
+
in: sanitizedUserIds,
|
|
2115
|
+
},
|
|
2116
|
+
},
|
|
2117
|
+
select: {
|
|
2118
|
+
user_id: true,
|
|
2119
|
+
},
|
|
2120
|
+
});
|
|
2121
|
+
|
|
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
|
+
);
|
|
2127
|
+
|
|
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
|
+
}
|
|
2137
|
+
|
|
2138
|
+
return {
|
|
2139
|
+
success: true,
|
|
2140
|
+
sharedCount: newUserIds.length,
|
|
2141
|
+
sharedUserIds: newUserIds,
|
|
2142
|
+
alreadySharedUserIds,
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
async revokeDashboardShare(
|
|
2147
|
+
userId: number,
|
|
2148
|
+
slug: string,
|
|
2149
|
+
sharedUserId: number,
|
|
2150
|
+
locale: string,
|
|
2151
|
+
) {
|
|
2152
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
2153
|
+
|
|
2154
|
+
if (sharedUserId === userId) {
|
|
2155
|
+
throw new BadRequestException(
|
|
2156
|
+
getLocaleText(
|
|
2157
|
+
'validation.invalidValue',
|
|
2158
|
+
locale,
|
|
2159
|
+
'Use the remove dashboard action to leave this tab.',
|
|
2160
|
+
),
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
await this.prismaService.dashboard_user.deleteMany({
|
|
2165
|
+
where: {
|
|
2166
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
2167
|
+
user_id: sharedUserId,
|
|
2168
|
+
},
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
return {
|
|
2172
|
+
success: true,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
async removeUserDashboard(userId: number, slug: string, locale: string) {
|
|
2177
|
+
const dashboardUser = await this.getDashboardUserOrThrow(userId, slug, locale);
|
|
2178
|
+
|
|
2179
|
+
await this.prismaService.dashboard_user.delete({
|
|
2180
|
+
where: { id: dashboardUser.id },
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
if (dashboardUser.is_home) {
|
|
2184
|
+
const nextDashboard = await this.prismaService.dashboard_user.findFirst({
|
|
2185
|
+
where: { user_id: userId },
|
|
2186
|
+
orderBy: { id: 'asc' },
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
if (nextDashboard) {
|
|
2190
|
+
await this.prismaService.dashboard_user.update({
|
|
2191
|
+
where: { id: nextDashboard.id },
|
|
2192
|
+
data: { is_home: true },
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const remainingShares = await this.prismaService.dashboard_user.count({
|
|
2198
|
+
where: {
|
|
2199
|
+
dashboard_id: dashboardUser.dashboard_id,
|
|
2200
|
+
},
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
if (remainingShares === 0) {
|
|
2204
|
+
await this.prismaService.dashboard.delete({
|
|
2205
|
+
where: { id: dashboardUser.dashboard_id },
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
return {
|
|
2210
|
+
success: true,
|
|
2211
|
+
removedSlug: slug,
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
|
|
1359
2215
|
async getAccountSecurity(userId: number) {
|
|
1360
2216
|
const now = new Date();
|
|
1361
2217
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|