@oneuptime/common 10.5.8 → 10.5.9
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/Models/DatabaseModels/SmsLog.ts +111 -0
- package/Server/API/DashboardAPI.ts +616 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Types/SmsStatus.ts +16 -0
- package/build/dist/Models/DatabaseModels/SmsLog.js +112 -0
- package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +459 -2
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Types/SmsStatus.js +15 -0
- package/build/dist/Types/SmsStatus.js.map +1 -1
- package/package.json +2 -2
|
@@ -26,6 +26,219 @@ import ForbiddenException from "../../Types/Exception/ForbiddenException";
|
|
|
26
26
|
import JSONFunctions from "../../Types/JSONFunctions";
|
|
27
27
|
import TelemetryAttributeService from "../Services/TelemetryAttributeService";
|
|
28
28
|
import TelemetryType from "../../Types/Telemetry/TelemetryType";
|
|
29
|
+
import { JSONObject } from "../../Types/JSON";
|
|
30
|
+
import AggregateBy from "../Types/AnalyticsDatabase/AggregateBy";
|
|
31
|
+
import AggregatedResult from "../../Types/BaseDatabase/AggregatedResult";
|
|
32
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
33
|
+
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
34
|
+
import Metric from "../../Models/AnalyticsModels/Metric";
|
|
35
|
+
import MetricType from "../../Models/DatabaseModels/MetricType";
|
|
36
|
+
import MetricService from "../Services/MetricService";
|
|
37
|
+
import MetricTypeService from "../Services/MetricTypeService";
|
|
38
|
+
import PositiveNumber from "../../Types/PositiveNumber";
|
|
39
|
+
import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
40
|
+
import AnalyticsDataModel from "../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
41
|
+
import Incident from "../../Models/DatabaseModels/Incident";
|
|
42
|
+
import Alert from "../../Models/DatabaseModels/Alert";
|
|
43
|
+
import Monitor from "../../Models/DatabaseModels/Monitor";
|
|
44
|
+
import Host from "../../Models/DatabaseModels/Host";
|
|
45
|
+
import KubernetesResource from "../../Models/DatabaseModels/KubernetesResource";
|
|
46
|
+
import DockerHost from "../../Models/DatabaseModels/DockerHost";
|
|
47
|
+
import DockerResource from "../../Models/DatabaseModels/DockerResource";
|
|
48
|
+
import Span from "../../Models/AnalyticsModels/Span";
|
|
49
|
+
import Log from "../../Models/AnalyticsModels/Log";
|
|
50
|
+
import IncidentService from "../Services/IncidentService";
|
|
51
|
+
import AlertService from "../Services/AlertService";
|
|
52
|
+
import MonitorService from "../Services/MonitorService";
|
|
53
|
+
import HostService from "../Services/HostService";
|
|
54
|
+
import KubernetesResourceService from "../Services/KubernetesResourceService";
|
|
55
|
+
import DockerHostService from "../Services/DockerHostService";
|
|
56
|
+
import DockerResourceService from "../Services/DockerResourceService";
|
|
57
|
+
import SpanService from "../Services/SpanService";
|
|
58
|
+
import LogService from "../Services/LogService";
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Registry of the non-metric widgets a public dashboard may render. The
|
|
62
|
+
* `select` is the FIXED set of columns the corresponding widget displays —
|
|
63
|
+
* the public list endpoint ignores any client-supplied select and uses this,
|
|
64
|
+
* so an anonymous viewer can only ever read these columns (and only for the
|
|
65
|
+
* dashboard's own project). Adding a widget to a public dashboard is the
|
|
66
|
+
* owner's explicit opt-in to exposing these columns.
|
|
67
|
+
*/
|
|
68
|
+
interface PublicDashboardResourceConfig {
|
|
69
|
+
modelType: { new (): BaseModel | AnalyticsDataModel };
|
|
70
|
+
service: {
|
|
71
|
+
findBy: (findBy: any) => Promise<Array<BaseModel | AnalyticsDataModel>>;
|
|
72
|
+
};
|
|
73
|
+
select: JSONObject;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_DASHBOARD_RESOURCE_LIMIT: number = 100;
|
|
77
|
+
|
|
78
|
+
const PUBLIC_DASHBOARD_RESOURCES: Record<
|
|
79
|
+
string,
|
|
80
|
+
PublicDashboardResourceConfig
|
|
81
|
+
> = {
|
|
82
|
+
incident: {
|
|
83
|
+
modelType: Incident,
|
|
84
|
+
service: IncidentService,
|
|
85
|
+
select: {
|
|
86
|
+
_id: true,
|
|
87
|
+
title: true,
|
|
88
|
+
createdAt: true,
|
|
89
|
+
currentIncidentState: { name: true, color: true },
|
|
90
|
+
incidentSeverity: { name: true, color: true },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
alert: {
|
|
94
|
+
modelType: Alert,
|
|
95
|
+
service: AlertService,
|
|
96
|
+
select: {
|
|
97
|
+
_id: true,
|
|
98
|
+
title: true,
|
|
99
|
+
createdAt: true,
|
|
100
|
+
currentAlertState: { name: true, color: true },
|
|
101
|
+
alertSeverity: { name: true, color: true },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
monitor: {
|
|
105
|
+
modelType: Monitor,
|
|
106
|
+
service: MonitorService,
|
|
107
|
+
select: {
|
|
108
|
+
_id: true,
|
|
109
|
+
name: true,
|
|
110
|
+
monitorType: true,
|
|
111
|
+
currentMonitorStatus: { name: true, color: true },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
host: {
|
|
115
|
+
modelType: Host,
|
|
116
|
+
service: HostService,
|
|
117
|
+
select: {
|
|
118
|
+
_id: true,
|
|
119
|
+
name: true,
|
|
120
|
+
hostIdentifier: true,
|
|
121
|
+
otelCollectorStatus: true,
|
|
122
|
+
osType: true,
|
|
123
|
+
osVersion: true,
|
|
124
|
+
cpuCores: true,
|
|
125
|
+
totalMemoryBytes: true,
|
|
126
|
+
lastSeenAt: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
"kubernetes-resource": {
|
|
130
|
+
modelType: KubernetesResource,
|
|
131
|
+
service: KubernetesResourceService,
|
|
132
|
+
select: {
|
|
133
|
+
_id: true,
|
|
134
|
+
name: true,
|
|
135
|
+
namespaceKey: true,
|
|
136
|
+
kind: true,
|
|
137
|
+
phase: true,
|
|
138
|
+
isReady: true,
|
|
139
|
+
hasMemoryPressure: true,
|
|
140
|
+
hasDiskPressure: true,
|
|
141
|
+
hasPidPressure: true,
|
|
142
|
+
containerCount: true,
|
|
143
|
+
latestCpuPercent: true,
|
|
144
|
+
latestMemoryBytes: true,
|
|
145
|
+
controllerDeploymentName: true,
|
|
146
|
+
controllerCronJobName: true,
|
|
147
|
+
resourceCreationTimestamp: true,
|
|
148
|
+
lastSeenAt: true,
|
|
149
|
+
kubernetesClusterId: true,
|
|
150
|
+
kubernetesCluster: { name: true },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
"docker-host": {
|
|
154
|
+
modelType: DockerHost,
|
|
155
|
+
service: DockerHostService,
|
|
156
|
+
select: {
|
|
157
|
+
_id: true,
|
|
158
|
+
name: true,
|
|
159
|
+
otelCollectorStatus: true,
|
|
160
|
+
containersRunning: true,
|
|
161
|
+
containersStopped: true,
|
|
162
|
+
containersPaused: true,
|
|
163
|
+
osType: true,
|
|
164
|
+
osVersion: true,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"docker-container": {
|
|
168
|
+
modelType: DockerResource,
|
|
169
|
+
service: DockerResourceService,
|
|
170
|
+
select: {
|
|
171
|
+
_id: true,
|
|
172
|
+
name: true,
|
|
173
|
+
imageName: true,
|
|
174
|
+
state: true,
|
|
175
|
+
latestCpuPercent: true,
|
|
176
|
+
latestMemoryBytes: true,
|
|
177
|
+
dockerHostId: true,
|
|
178
|
+
dockerHost: { name: true },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
"docker-image": {
|
|
182
|
+
modelType: DockerResource,
|
|
183
|
+
service: DockerResourceService,
|
|
184
|
+
select: {
|
|
185
|
+
_id: true,
|
|
186
|
+
name: true,
|
|
187
|
+
containerId: true,
|
|
188
|
+
dockerHostId: true,
|
|
189
|
+
dockerHost: { name: true },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
"docker-network": {
|
|
193
|
+
modelType: DockerResource,
|
|
194
|
+
service: DockerResourceService,
|
|
195
|
+
select: {
|
|
196
|
+
_id: true,
|
|
197
|
+
name: true,
|
|
198
|
+
state: true,
|
|
199
|
+
dockerHostId: true,
|
|
200
|
+
dockerHost: { name: true },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"docker-volume": {
|
|
204
|
+
modelType: DockerResource,
|
|
205
|
+
service: DockerResourceService,
|
|
206
|
+
select: {
|
|
207
|
+
_id: true,
|
|
208
|
+
name: true,
|
|
209
|
+
state: true,
|
|
210
|
+
dockerHostId: true,
|
|
211
|
+
dockerHost: { name: true },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
span: {
|
|
215
|
+
modelType: Span,
|
|
216
|
+
service: SpanService,
|
|
217
|
+
select: {
|
|
218
|
+
startTime: true,
|
|
219
|
+
name: true,
|
|
220
|
+
statusCode: true,
|
|
221
|
+
durationUnixNano: true,
|
|
222
|
+
traceId: true,
|
|
223
|
+
spanId: true,
|
|
224
|
+
kind: true,
|
|
225
|
+
serviceId: true,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
log: {
|
|
229
|
+
modelType: Log,
|
|
230
|
+
service: LogService,
|
|
231
|
+
select: {
|
|
232
|
+
time: true,
|
|
233
|
+
severityText: true,
|
|
234
|
+
body: true,
|
|
235
|
+
serviceId: true,
|
|
236
|
+
traceId: true,
|
|
237
|
+
spanId: true,
|
|
238
|
+
attributes: true,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
};
|
|
29
242
|
|
|
30
243
|
export default class DashboardAPI extends BaseAPI<
|
|
31
244
|
Dashboard,
|
|
@@ -392,6 +605,268 @@ export default class DashboardAPI extends BaseAPI<
|
|
|
392
605
|
},
|
|
393
606
|
);
|
|
394
607
|
|
|
608
|
+
/*
|
|
609
|
+
* Public metric-type lookup for dashboard charts.
|
|
610
|
+
*
|
|
611
|
+
* The private `/metric-type/get-list` CRUD route requires a logged-in
|
|
612
|
+
* session with Telemetry read permission; public dashboards have no
|
|
613
|
+
* session, so the shared chart code used to fall through to it, 401 →
|
|
614
|
+
* the global API error handler redirected the viewer to /accounts/login.
|
|
615
|
+
* Mirror it here scoped to the dashboard's owning projectId.
|
|
616
|
+
* Authorization reuses DashboardService.hasReadAccess (public flag, IP
|
|
617
|
+
* whitelist, master password) — never falls back to project-wide read.
|
|
618
|
+
*/
|
|
619
|
+
this.router.post(
|
|
620
|
+
`${new this.entityType()
|
|
621
|
+
.getCrudApiPath()
|
|
622
|
+
?.toString()}/metric-types/:dashboardId`,
|
|
623
|
+
UserMiddleware.getUserMiddleware,
|
|
624
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
625
|
+
try {
|
|
626
|
+
const dashboardId: ObjectID = new ObjectID(
|
|
627
|
+
req.params["dashboardId"] as string,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const accessResult: {
|
|
631
|
+
hasReadAccess: boolean;
|
|
632
|
+
error?: NotAuthenticatedException | ForbiddenException;
|
|
633
|
+
} = await DashboardService.hasReadAccess({
|
|
634
|
+
dashboardId,
|
|
635
|
+
req,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!accessResult.hasReadAccess) {
|
|
639
|
+
throw (
|
|
640
|
+
accessResult.error ||
|
|
641
|
+
new BadDataException("Access denied to this dashboard.")
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const dashboard: Dashboard | null =
|
|
646
|
+
await DashboardService.findOneById({
|
|
647
|
+
id: dashboardId,
|
|
648
|
+
select: {
|
|
649
|
+
_id: true,
|
|
650
|
+
projectId: true,
|
|
651
|
+
dashboardViewConfig: true,
|
|
652
|
+
},
|
|
653
|
+
props: {
|
|
654
|
+
isRoot: true,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!dashboard || !dashboard.projectId) {
|
|
659
|
+
throw new NotFoundException("Dashboard not found");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/*
|
|
663
|
+
* Only expose metric types the dashboard actually charts, so a
|
|
664
|
+
* public viewer cannot enumerate the owning project's full metric
|
|
665
|
+
* catalog.
|
|
666
|
+
*/
|
|
667
|
+
const allowedMetricNames: Set<string> =
|
|
668
|
+
DashboardAPI.collectDashboardMetricNames(
|
|
669
|
+
dashboard.dashboardViewConfig,
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
if (allowedMetricNames.size === 0) {
|
|
673
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
674
|
+
metricTypes: [],
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const metricTypes: Array<MetricType> = await MetricTypeService.findBy(
|
|
679
|
+
{
|
|
680
|
+
query: {
|
|
681
|
+
projectId: dashboard.projectId,
|
|
682
|
+
},
|
|
683
|
+
select: {
|
|
684
|
+
name: true,
|
|
685
|
+
unit: true,
|
|
686
|
+
},
|
|
687
|
+
skip: 0,
|
|
688
|
+
limit: LIMIT_PER_PROJECT,
|
|
689
|
+
sort: {
|
|
690
|
+
name: SortOrder.Ascending,
|
|
691
|
+
},
|
|
692
|
+
props: {
|
|
693
|
+
isRoot: true,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
699
|
+
metricTypes: metricTypes
|
|
700
|
+
.filter((metricType: MetricType) => {
|
|
701
|
+
return Boolean(
|
|
702
|
+
metricType.name && allowedMetricNames.has(metricType.name),
|
|
703
|
+
);
|
|
704
|
+
})
|
|
705
|
+
.map((metricType: MetricType) => {
|
|
706
|
+
return {
|
|
707
|
+
name: metricType.name || "",
|
|
708
|
+
unit: metricType.unit || "",
|
|
709
|
+
};
|
|
710
|
+
}),
|
|
711
|
+
});
|
|
712
|
+
} catch (err) {
|
|
713
|
+
next(err);
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
/*
|
|
719
|
+
* Public metric aggregation for dashboard charts/values/gauges/tables.
|
|
720
|
+
*
|
|
721
|
+
* Mirrors the private `/metrics/aggregate` route (which requires a
|
|
722
|
+
* logged-in session). The client-supplied projectId is IGNORED and the
|
|
723
|
+
* aggregation is pinned to the dashboard's owning projectId, so a public
|
|
724
|
+
* viewer can only read metrics belonging to this dashboard's project and
|
|
725
|
+
* never another tenant's. Authorization reuses
|
|
726
|
+
* DashboardService.hasReadAccess.
|
|
727
|
+
*/
|
|
728
|
+
this.router.post(
|
|
729
|
+
`${new this.entityType()
|
|
730
|
+
.getCrudApiPath()
|
|
731
|
+
?.toString()}/metrics-aggregate/:dashboardId`,
|
|
732
|
+
UserMiddleware.getUserMiddleware,
|
|
733
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
734
|
+
try {
|
|
735
|
+
const dashboardId: ObjectID = new ObjectID(
|
|
736
|
+
req.params["dashboardId"] as string,
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
const accessResult: {
|
|
740
|
+
hasReadAccess: boolean;
|
|
741
|
+
error?: NotAuthenticatedException | ForbiddenException;
|
|
742
|
+
} = await DashboardService.hasReadAccess({
|
|
743
|
+
dashboardId,
|
|
744
|
+
req,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (!accessResult.hasReadAccess) {
|
|
748
|
+
throw (
|
|
749
|
+
accessResult.error ||
|
|
750
|
+
new BadDataException("Access denied to this dashboard.")
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (!req.body || !req.body["aggregateBy"]) {
|
|
755
|
+
throw new BadDataException("aggregateBy is required.");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const dashboard: Dashboard | null =
|
|
759
|
+
await DashboardService.findOneById({
|
|
760
|
+
id: dashboardId,
|
|
761
|
+
select: {
|
|
762
|
+
_id: true,
|
|
763
|
+
projectId: true,
|
|
764
|
+
dashboardViewConfig: true,
|
|
765
|
+
},
|
|
766
|
+
props: {
|
|
767
|
+
isRoot: true,
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
if (!dashboard || !dashboard.projectId) {
|
|
772
|
+
throw new NotFoundException("Dashboard not found");
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const aggregateBy: AggregateBy<Metric> = {
|
|
776
|
+
...(JSONFunctions.deserialize(
|
|
777
|
+
req.body["aggregateBy"] as JSONObject,
|
|
778
|
+
) as unknown as AggregateBy<Metric>),
|
|
779
|
+
/*
|
|
780
|
+
* Run as root: authorization is already enforced by hasReadAccess
|
|
781
|
+
* above and the project scope is pinned below.
|
|
782
|
+
*/
|
|
783
|
+
props: {
|
|
784
|
+
isRoot: true,
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
/*
|
|
789
|
+
* Restrict aggregation to the metric names this dashboard actually
|
|
790
|
+
* charts. Without this, a public viewer who knows the dashboard ID
|
|
791
|
+
* could aggregate any metric in the owning project. Variable
|
|
792
|
+
* interpolation only rewrites attribute filters, never the metric
|
|
793
|
+
* name, so the stored view config is an exact allowlist.
|
|
794
|
+
*/
|
|
795
|
+
const allowedMetricNames: Set<string> =
|
|
796
|
+
DashboardAPI.collectDashboardMetricNames(
|
|
797
|
+
dashboard.dashboardViewConfig,
|
|
798
|
+
);
|
|
799
|
+
const requestedMetricName: unknown = aggregateBy.query
|
|
800
|
+
? (aggregateBy.query as Record<string, unknown>)["name"]
|
|
801
|
+
: undefined;
|
|
802
|
+
|
|
803
|
+
if (
|
|
804
|
+
typeof requestedMetricName !== "string" ||
|
|
805
|
+
!allowedMetricNames.has(requestedMetricName)
|
|
806
|
+
) {
|
|
807
|
+
throw new BadDataException(
|
|
808
|
+
"This metric is not part of this dashboard.",
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/*
|
|
813
|
+
* Security: never trust a client-supplied projectId on a public,
|
|
814
|
+
* unauthenticated endpoint. Pin the aggregation to the dashboard's
|
|
815
|
+
* project before it reaches the database service.
|
|
816
|
+
*/
|
|
817
|
+
aggregateBy.query = {
|
|
818
|
+
...(aggregateBy.query || {}),
|
|
819
|
+
projectId: dashboard.projectId,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const aggregateResult: AggregatedResult =
|
|
823
|
+
await MetricService.aggregateBy(aggregateBy);
|
|
824
|
+
|
|
825
|
+
const responseBody: JSONObject = {
|
|
826
|
+
...(aggregateResult as unknown as JSONObject),
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
return Response.sendJsonObjectResponse(req, res, responseBody);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
next(err);
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
/*
|
|
837
|
+
* Public resource lists for non-metric dashboard widgets (incident /
|
|
838
|
+
* alert / monitor / trace / log / kubernetes / docker / host lists).
|
|
839
|
+
*
|
|
840
|
+
* Each widget renders a fixed set of columns; the server pins the select
|
|
841
|
+
* to exactly those columns (see PUBLIC_DASHBOARD_RESOURCES) and forces the
|
|
842
|
+
* project scope to the dashboard's project, so a public viewer can only
|
|
843
|
+
* read the data the widget was built to show, for this dashboard's
|
|
844
|
+
* project. Authorization reuses DashboardService.hasReadAccess.
|
|
845
|
+
*/
|
|
846
|
+
this.router.post(
|
|
847
|
+
`${new this.entityType()
|
|
848
|
+
.getCrudApiPath()
|
|
849
|
+
?.toString()}/resource-list/:dashboardId/:resourceType`,
|
|
850
|
+
UserMiddleware.getUserMiddleware,
|
|
851
|
+
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
|
|
852
|
+
try {
|
|
853
|
+
const resourceType: string = req.params["resourceType"] as string;
|
|
854
|
+
const config: PublicDashboardResourceConfig | undefined =
|
|
855
|
+
PUBLIC_DASHBOARD_RESOURCES[resourceType];
|
|
856
|
+
|
|
857
|
+
if (!config) {
|
|
858
|
+
throw new BadDataException(
|
|
859
|
+
`Unsupported dashboard resource type: ${resourceType}`,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
await DashboardAPI.servePublicResourceList({ req, res, config });
|
|
864
|
+
} catch (err) {
|
|
865
|
+
next(err);
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
);
|
|
869
|
+
|
|
395
870
|
this.router.post(
|
|
396
871
|
`${new this.entityType()
|
|
397
872
|
.getCrudApiPath()
|
|
@@ -469,6 +944,147 @@ export default class DashboardAPI extends BaseAPI<
|
|
|
469
944
|
);
|
|
470
945
|
}
|
|
471
946
|
|
|
947
|
+
/*
|
|
948
|
+
* Walk a stored dashboard view config and collect every metric name it
|
|
949
|
+
* references (chart/value/gauge/table widgets all carry their metric in a
|
|
950
|
+
* `metricName` field somewhere under their query config). Used to build an
|
|
951
|
+
* allowlist for the public metric endpoints so an anonymous viewer can only
|
|
952
|
+
* read the metrics this dashboard was built to show.
|
|
953
|
+
*/
|
|
954
|
+
private static collectDashboardMetricNames(
|
|
955
|
+
dashboardViewConfig: unknown,
|
|
956
|
+
): Set<string> {
|
|
957
|
+
const metricNames: Set<string> = new Set<string>();
|
|
958
|
+
|
|
959
|
+
const walk: (node: unknown) => void = (node: unknown): void => {
|
|
960
|
+
if (!node || typeof node !== "object") {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (Array.isArray(node)) {
|
|
965
|
+
for (const item of node) {
|
|
966
|
+
walk(item);
|
|
967
|
+
}
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const obj: Record<string, unknown> = node as Record<string, unknown>;
|
|
972
|
+
|
|
973
|
+
const metricName: unknown = obj["metricName"];
|
|
974
|
+
if (typeof metricName === "string" && metricName.trim().length > 0) {
|
|
975
|
+
metricNames.add(metricName);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
for (const key of Object.keys(obj)) {
|
|
979
|
+
walk(obj[key]);
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
walk(dashboardViewConfig);
|
|
984
|
+
|
|
985
|
+
return metricNames;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/*
|
|
989
|
+
* Shared handler for the public resource-list endpoint. Loads the
|
|
990
|
+
* dashboard, enforces read access, pins the query to the dashboard's
|
|
991
|
+
* project, and lists the resource using the registry's FIXED select.
|
|
992
|
+
*/
|
|
993
|
+
private static async servePublicResourceList(data: {
|
|
994
|
+
req: ExpressRequest;
|
|
995
|
+
res: ExpressResponse;
|
|
996
|
+
config: PublicDashboardResourceConfig;
|
|
997
|
+
}): Promise<void> {
|
|
998
|
+
const { req, res, config } = data;
|
|
999
|
+
|
|
1000
|
+
const dashboardId: ObjectID = new ObjectID(
|
|
1001
|
+
req.params["dashboardId"] as string,
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
const accessResult: {
|
|
1005
|
+
hasReadAccess: boolean;
|
|
1006
|
+
error?: NotAuthenticatedException | ForbiddenException;
|
|
1007
|
+
} = await DashboardService.hasReadAccess({ dashboardId, req });
|
|
1008
|
+
|
|
1009
|
+
if (!accessResult.hasReadAccess) {
|
|
1010
|
+
throw (
|
|
1011
|
+
accessResult.error ||
|
|
1012
|
+
new BadDataException("Access denied to this dashboard.")
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const dashboard: Dashboard | null = await DashboardService.findOneById({
|
|
1017
|
+
id: dashboardId,
|
|
1018
|
+
select: {
|
|
1019
|
+
_id: true,
|
|
1020
|
+
projectId: true,
|
|
1021
|
+
},
|
|
1022
|
+
props: {
|
|
1023
|
+
isRoot: true,
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
if (!dashboard || !dashboard.projectId) {
|
|
1028
|
+
throw new NotFoundException("Dashboard not found");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const query: JSONObject =
|
|
1032
|
+
req.body && req.body["query"]
|
|
1033
|
+
? (JSONFunctions.deserialize(
|
|
1034
|
+
req.body["query"] as JSONObject,
|
|
1035
|
+
) as JSONObject)
|
|
1036
|
+
: {};
|
|
1037
|
+
|
|
1038
|
+
/*
|
|
1039
|
+
* Security: pin to the dashboard's project; never trust a client-supplied
|
|
1040
|
+
* projectId on a public, unauthenticated endpoint.
|
|
1041
|
+
*/
|
|
1042
|
+
(query as Record<string, unknown>)["projectId"] = dashboard.projectId;
|
|
1043
|
+
|
|
1044
|
+
const sort: JSONObject =
|
|
1045
|
+
req.body && req.body["sort"]
|
|
1046
|
+
? (JSONFunctions.deserialize(
|
|
1047
|
+
req.body["sort"] as JSONObject,
|
|
1048
|
+
) as JSONObject)
|
|
1049
|
+
: {};
|
|
1050
|
+
|
|
1051
|
+
const requestedLimit: number = req.query["limit"]
|
|
1052
|
+
? parseInt(req.query["limit"] as string, 10)
|
|
1053
|
+
: DEFAULT_DASHBOARD_RESOURCE_LIMIT;
|
|
1054
|
+
const limit: number = Math.min(
|
|
1055
|
+
Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
1056
|
+
? requestedLimit
|
|
1057
|
+
: DEFAULT_DASHBOARD_RESOURCE_LIMIT,
|
|
1058
|
+
LIMIT_PER_PROJECT,
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
const requestedSkip: number = req.query["skip"]
|
|
1062
|
+
? parseInt(req.query["skip"] as string, 10)
|
|
1063
|
+
: 0;
|
|
1064
|
+
const skip: number =
|
|
1065
|
+
Number.isFinite(requestedSkip) && requestedSkip > 0 ? requestedSkip : 0;
|
|
1066
|
+
|
|
1067
|
+
const list: Array<BaseModel | AnalyticsDataModel> =
|
|
1068
|
+
await config.service.findBy({
|
|
1069
|
+
query: query,
|
|
1070
|
+
select: config.select,
|
|
1071
|
+
sort: sort,
|
|
1072
|
+
limit: limit,
|
|
1073
|
+
skip: skip,
|
|
1074
|
+
props: {
|
|
1075
|
+
isRoot: true,
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
return Response.sendEntityArrayResponse(
|
|
1080
|
+
req,
|
|
1081
|
+
res,
|
|
1082
|
+
list,
|
|
1083
|
+
new PositiveNumber(list.length),
|
|
1084
|
+
config.modelType,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
472
1088
|
private static getFileAsBase64JSONObject(
|
|
473
1089
|
file: any,
|
|
474
1090
|
): { file: string; fileType: string } | null {
|
package/Server/Infrastructure/Postgres/SchemaMigrations/1780317745887-AddDeliveryTrackingToSmsLog.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
2
|
+
|
|
3
|
+
export class AddDeliveryTrackingToSmsLog1780317745887
|
|
4
|
+
implements MigrationInterface
|
|
5
|
+
{
|
|
6
|
+
public name: string = "AddDeliveryTrackingToSmsLog1780317745887";
|
|
7
|
+
|
|
8
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
9
|
+
await queryRunner.query(
|
|
10
|
+
`ALTER TABLE "SmsLog" ADD "errorCode" character varying(100)`,
|
|
11
|
+
);
|
|
12
|
+
await queryRunner.query(
|
|
13
|
+
`ALTER TABLE "SmsLog" ADD "statusCallbackToken" character varying(100)`,
|
|
14
|
+
);
|
|
15
|
+
await queryRunner.query(
|
|
16
|
+
`ALTER TABLE "SmsLog" ADD "userOnCallLogTimelineId" uuid`,
|
|
17
|
+
);
|
|
18
|
+
await queryRunner.query(
|
|
19
|
+
`CREATE INDEX "IDX_SmsLog_userOnCallLogTimelineId" ON "SmsLog" ("userOnCallLogTimelineId") `,
|
|
20
|
+
);
|
|
21
|
+
await queryRunner.query(
|
|
22
|
+
`ALTER TABLE "SmsLog" ADD CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId" FOREIGN KEY ("userOnCallLogTimelineId") REFERENCES "UserOnCallLogTimeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
27
|
+
await queryRunner.query(
|
|
28
|
+
`ALTER TABLE "SmsLog" DROP CONSTRAINT "FK_SmsLog_userOnCallLogTimelineId"`,
|
|
29
|
+
);
|
|
30
|
+
await queryRunner.query(`DROP INDEX "IDX_SmsLog_userOnCallLogTimelineId"`);
|
|
31
|
+
await queryRunner.query(
|
|
32
|
+
`ALTER TABLE "SmsLog" DROP COLUMN "userOnCallLogTimelineId"`,
|
|
33
|
+
);
|
|
34
|
+
await queryRunner.query(
|
|
35
|
+
`ALTER TABLE "SmsLog" DROP COLUMN "statusCallbackToken"`,
|
|
36
|
+
);
|
|
37
|
+
await queryRunner.query(`ALTER TABLE "SmsLog" DROP COLUMN "errorCode"`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -360,6 +360,7 @@ import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-Add
|
|
|
360
360
|
import { AddTransportTypeToProjectSmtpConfig1779975064262 } from "./1779975064262-AddTransportTypeToProjectSmtpConfig";
|
|
361
361
|
import { AddSmtpTransportTypeToGlobalConfig1779976190561 } from "./1779976190561-AddSmtpTransportTypeToGlobalConfig";
|
|
362
362
|
import { MigrationName1779980428744 } from "./1779980428744-MigrationName";
|
|
363
|
+
import { AddDeliveryTrackingToSmsLog1780317745887 } from "./1780317745887-AddDeliveryTrackingToSmsLog";
|
|
363
364
|
|
|
364
365
|
export default [
|
|
365
366
|
InitialMigration,
|
|
@@ -724,4 +725,5 @@ export default [
|
|
|
724
725
|
AddTransportTypeToProjectSmtpConfig1779975064262,
|
|
725
726
|
AddSmtpTransportTypeToGlobalConfig1779976190561,
|
|
726
727
|
MigrationName1779980428744,
|
|
728
|
+
AddDeliveryTrackingToSmsLog1780317745887,
|
|
727
729
|
];
|