@oneuptime/common 10.4.13 → 10.4.14

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 (103) hide show
  1. package/Models/DatabaseModels/Alert.ts +2 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +1 -0
  3. package/Models/DatabaseModels/CallLog.ts +2 -0
  4. package/Models/DatabaseModels/DockerHost.ts +34 -0
  5. package/Models/DatabaseModels/EmailLog.ts +2 -0
  6. package/Models/DatabaseModels/Host.ts +34 -0
  7. package/Models/DatabaseModels/Incident.ts +1 -0
  8. package/Models/DatabaseModels/IncidentFeed.ts +1 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +34 -0
  10. package/Models/DatabaseModels/MonitorFeed.ts +1 -0
  11. package/Models/DatabaseModels/MonitorProbe.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +3 -0
  13. package/Models/DatabaseModels/SmsLog.ts +2 -0
  14. package/Models/DatabaseModels/StatusPageSubscriber.ts +2 -0
  15. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  16. package/Models/DatabaseModels/UserOnCallLog.ts +1 -0
  17. package/Models/DatabaseModels/WorkflowLog.ts +1 -0
  18. package/Server/API/ProjectAPI.ts +52 -15
  19. package/Server/EnvironmentConfig.ts +69 -0
  20. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +26 -1
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.ts +29 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.ts +160 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  24. package/Server/Infrastructure/PostgresDatabase.ts +2 -5
  25. package/Server/Middleware/ProjectAuthorization.ts +31 -53
  26. package/Server/Middleware/UserAuthorization.ts +106 -64
  27. package/Server/Services/ApiKeyService.ts +100 -1
  28. package/Server/Services/DockerHostService.ts +5 -0
  29. package/Server/Services/HostService.ts +6 -0
  30. package/Server/Services/KubernetesClusterService.ts +33 -10
  31. package/Server/Services/MonitorService.ts +10 -3
  32. package/Server/Services/ProjectService.ts +72 -1
  33. package/Server/Services/TeamMemberService.ts +36 -0
  34. package/Server/Services/UserService.ts +38 -0
  35. package/build/dist/Models/DatabaseModels/Alert.js +3 -1
  36. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  37. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -1
  38. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  39. package/build/dist/Models/DatabaseModels/CallLog.js +4 -1
  40. package/build/dist/Models/DatabaseModels/CallLog.js.map +1 -1
  41. package/build/dist/Models/DatabaseModels/DockerHost.js +35 -0
  42. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  43. package/build/dist/Models/DatabaseModels/EmailLog.js +4 -1
  44. package/build/dist/Models/DatabaseModels/EmailLog.js.map +1 -1
  45. package/build/dist/Models/DatabaseModels/Host.js +35 -0
  46. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  47. package/build/dist/Models/DatabaseModels/Incident.js +2 -1
  48. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -1
  50. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +35 -0
  52. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  53. package/build/dist/Models/DatabaseModels/MonitorFeed.js +2 -1
  54. package/build/dist/Models/DatabaseModels/MonitorFeed.js.map +1 -1
  55. package/build/dist/Models/DatabaseModels/MonitorProbe.js +2 -0
  56. package/build/dist/Models/DatabaseModels/MonitorProbe.js.map +1 -1
  57. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +3 -0
  58. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  59. package/build/dist/Models/DatabaseModels/SmsLog.js +4 -1
  60. package/build/dist/Models/DatabaseModels/SmsLog.js.map +1 -1
  61. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +4 -1
  62. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/TelemetryException.js +3 -1
  64. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/UserOnCallLog.js +1 -0
  66. package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/WorkflowLog.js +2 -1
  68. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  69. package/build/dist/Server/API/ProjectAPI.js +42 -14
  70. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  71. package/build/dist/Server/EnvironmentConfig.js +41 -0
  72. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  73. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +20 -2
  74. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.js +16 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392865146-AddAgentVersionToKubernetesDockerHost.js.map +1 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.js +63 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779392970424-AddPerformanceIndexes.js.map +1 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  80. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  81. package/build/dist/Server/Infrastructure/PostgresDatabase.js +2 -2
  82. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  83. package/build/dist/Server/Middleware/ProjectAuthorization.js +21 -39
  84. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  85. package/build/dist/Server/Middleware/UserAuthorization.js +83 -50
  86. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  87. package/build/dist/Server/Services/ApiKeyService.js +86 -0
  88. package/build/dist/Server/Services/ApiKeyService.js.map +1 -1
  89. package/build/dist/Server/Services/DockerHostService.js +5 -1
  90. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  91. package/build/dist/Server/Services/HostService.js +5 -1
  92. package/build/dist/Server/Services/HostService.js.map +1 -1
  93. package/build/dist/Server/Services/KubernetesClusterService.js +21 -11
  94. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  95. package/build/dist/Server/Services/MonitorService.js +8 -3
  96. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  97. package/build/dist/Server/Services/ProjectService.js +65 -1
  98. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  99. package/build/dist/Server/Services/TeamMemberService.js +24 -0
  100. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  101. package/build/dist/Server/Services/UserService.js +36 -0
  102. package/build/dist/Server/Services/UserService.js.map +1 -1
  103. package/package.json +1 -1
@@ -0,0 +1,160 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddPerformanceIndexes1779392970424 implements MigrationInterface {
4
+ public name: string = "AddPerformanceIndexes1779392970424";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ // Active-X badge counters on the dashboard.
8
+ await queryRunner.query(
9
+ `CREATE INDEX "IDX_d846ce00a02d1073efc07178fa" ON "Incident" ("projectId", "currentIncidentStateId") `,
10
+ );
11
+ await queryRunner.query(
12
+ `CREATE INDEX "IDX_0c7286dfa90fd7d8201ec6f217" ON "Alert" ("projectId", "currentAlertStateId") `,
13
+ );
14
+
15
+ // Alert filtering by monitor (monitor detail page).
16
+ await queryRunner.query(
17
+ `CREATE INDEX "IDX_b57071fc2f1e27430e651382ee" ON "Alert" ("monitorId") `,
18
+ );
19
+
20
+ // Notification log tables: filter by project + status / time range.
21
+ await queryRunner.query(
22
+ `CREATE INDEX "IDX_8a0f032f20cd845c9bb908f38c" ON "CallLog" ("projectId", "status") `,
23
+ );
24
+ await queryRunner.query(
25
+ `CREATE INDEX "IDX_b59ee5f702882c10066cdd1128" ON "CallLog" ("projectId", "createdAt") `,
26
+ );
27
+ await queryRunner.query(
28
+ `CREATE INDEX "IDX_8f4eea9e7f20eaf121625f5787" ON "EmailLog" ("projectId", "status") `,
29
+ );
30
+ await queryRunner.query(
31
+ `CREATE INDEX "IDX_33d38c24249a256f89001acf83" ON "EmailLog" ("projectId", "createdAt") `,
32
+ );
33
+ await queryRunner.query(
34
+ `CREATE INDEX "IDX_c37d2fcfcb591afb284dad27d9" ON "SmsLog" ("projectId", "status") `,
35
+ );
36
+ await queryRunner.query(
37
+ `CREATE INDEX "IDX_c6bbede6b3bb66781bc7c82463" ON "SmsLog" ("projectId", "createdAt") `,
38
+ );
39
+
40
+ // Feed timelines on alert/incident/monitor detail pages.
41
+ await queryRunner.query(
42
+ `CREATE INDEX "IDX_6f97cb7c189e6339cf364a3608" ON "AlertFeed" ("alertId", "postedAt") `,
43
+ );
44
+ await queryRunner.query(
45
+ `CREATE INDEX "IDX_e26bf84ec503bbfb06bcde139e" ON "IncidentFeed" ("incidentId", "postedAt") `,
46
+ );
47
+ await queryRunner.query(
48
+ `CREATE INDEX "IDX_5771fc57305fb0f508153b53ce" ON "MonitorFeed" ("monitorId", "postedAt") `,
49
+ );
50
+
51
+ // Monitor probe scheduler.
52
+ await queryRunner.query(
53
+ `CREATE INDEX "IDX_4bf4109d325af5e5b5a5665bc7" ON "MonitorProbe" ("probeId", "isEnabled", "nextPingAt") `,
54
+ );
55
+
56
+ // On-call duty time logs (active-on-call lookups).
57
+ await queryRunner.query(
58
+ `CREATE INDEX "IDX_43da7ffeee531e9452d36a89ba" ON "OnCallDutyPolicyTimeLog" ("userId") `,
59
+ );
60
+ await queryRunner.query(
61
+ `CREATE INDEX "IDX_977d907fb45cbc1a2067f490af" ON "OnCallDutyPolicyTimeLog" ("startsAt") `,
62
+ );
63
+ await queryRunner.query(
64
+ `CREATE INDEX "IDX_002727a958120be971790fd016" ON "OnCallDutyPolicyTimeLog" ("endsAt") `,
65
+ );
66
+
67
+ // Telemetry exceptions dashboard.
68
+ await queryRunner.query(
69
+ `CREATE INDEX "IDX_fa102ae5073b428e514cc2ceea" ON "TelemetryException" ("occuranceCount") `,
70
+ );
71
+ await queryRunner.query(
72
+ `CREATE INDEX "IDX_3836772be478de8a9df86a938d" ON "TelemetryException" ("projectId", "isResolved", "isArchived") `,
73
+ );
74
+
75
+ // Status page subscriber dedupe.
76
+ await queryRunner.query(
77
+ `CREATE INDEX "IDX_c28628545faa67976e1d462e69" ON "StatusPageSubscriber" ("statusPageId", "subscriberPhone") `,
78
+ );
79
+ await queryRunner.query(
80
+ `CREATE INDEX "IDX_3a2d83fc5107c639d10a7c5cc0" ON "StatusPageSubscriber" ("statusPageId", "subscriberEmail") `,
81
+ );
82
+
83
+ // Worker sweeps.
84
+ await queryRunner.query(
85
+ `CREATE INDEX "IDX_094b044a3d6a79695ba754cdfb" ON "UserOnCallLog" ("status") `,
86
+ );
87
+ await queryRunner.query(
88
+ `CREATE INDEX "IDX_7a5dc4760803e57f2d0b363e6e" ON "WorkflowLog" ("workflowStatus", "createdAt") `,
89
+ );
90
+ }
91
+
92
+ public async down(queryRunner: QueryRunner): Promise<void> {
93
+ await queryRunner.query(
94
+ `DROP INDEX "public"."IDX_7a5dc4760803e57f2d0b363e6e"`,
95
+ );
96
+ await queryRunner.query(
97
+ `DROP INDEX "public"."IDX_094b044a3d6a79695ba754cdfb"`,
98
+ );
99
+ await queryRunner.query(
100
+ `DROP INDEX "public"."IDX_3a2d83fc5107c639d10a7c5cc0"`,
101
+ );
102
+ await queryRunner.query(
103
+ `DROP INDEX "public"."IDX_c28628545faa67976e1d462e69"`,
104
+ );
105
+ await queryRunner.query(
106
+ `DROP INDEX "public"."IDX_3836772be478de8a9df86a938d"`,
107
+ );
108
+ await queryRunner.query(
109
+ `DROP INDEX "public"."IDX_fa102ae5073b428e514cc2ceea"`,
110
+ );
111
+ await queryRunner.query(
112
+ `DROP INDEX "public"."IDX_002727a958120be971790fd016"`,
113
+ );
114
+ await queryRunner.query(
115
+ `DROP INDEX "public"."IDX_977d907fb45cbc1a2067f490af"`,
116
+ );
117
+ await queryRunner.query(
118
+ `DROP INDEX "public"."IDX_43da7ffeee531e9452d36a89ba"`,
119
+ );
120
+ await queryRunner.query(
121
+ `DROP INDEX "public"."IDX_4bf4109d325af5e5b5a5665bc7"`,
122
+ );
123
+ await queryRunner.query(
124
+ `DROP INDEX "public"."IDX_5771fc57305fb0f508153b53ce"`,
125
+ );
126
+ await queryRunner.query(
127
+ `DROP INDEX "public"."IDX_e26bf84ec503bbfb06bcde139e"`,
128
+ );
129
+ await queryRunner.query(
130
+ `DROP INDEX "public"."IDX_6f97cb7c189e6339cf364a3608"`,
131
+ );
132
+ await queryRunner.query(
133
+ `DROP INDEX "public"."IDX_c6bbede6b3bb66781bc7c82463"`,
134
+ );
135
+ await queryRunner.query(
136
+ `DROP INDEX "public"."IDX_c37d2fcfcb591afb284dad27d9"`,
137
+ );
138
+ await queryRunner.query(
139
+ `DROP INDEX "public"."IDX_33d38c24249a256f89001acf83"`,
140
+ );
141
+ await queryRunner.query(
142
+ `DROP INDEX "public"."IDX_8f4eea9e7f20eaf121625f5787"`,
143
+ );
144
+ await queryRunner.query(
145
+ `DROP INDEX "public"."IDX_b59ee5f702882c10066cdd1128"`,
146
+ );
147
+ await queryRunner.query(
148
+ `DROP INDEX "public"."IDX_8a0f032f20cd845c9bb908f38c"`,
149
+ );
150
+ await queryRunner.query(
151
+ `DROP INDEX "public"."IDX_b57071fc2f1e27430e651382ee"`,
152
+ );
153
+ await queryRunner.query(
154
+ `DROP INDEX "public"."IDX_0c7286dfa90fd7d8201ec6f217"`,
155
+ );
156
+ await queryRunner.query(
157
+ `DROP INDEX "public"."IDX_d846ce00a02d1073efc07178fa"`,
158
+ );
159
+ }
160
+ }
@@ -342,6 +342,8 @@ import { DropServiceDependencyTable1779277271302 } from "./1779277271302-DropSer
342
342
  import { AddTelemetryRetentionToHostDockerKubernetes1779282769946 } from "./1779282769946-AddTelemetryRetentionToHostDockerKubernetes";
343
343
  import { AttachKubernetesAndDockerToIncidentAndAlert1779302536475 } from "./1779302536475-AttachKubernetesAndDockerToIncidentAndAlert";
344
344
  import { AttachServiceToIncidentAndAlert1779303924241 } from "./1779303924241-AttachServiceToIncidentAndAlert";
345
+ import { AddAgentVersionToKubernetesDockerHost1779392865146 } from "./1779392865146-AddAgentVersionToKubernetesDockerHost";
346
+ import { AddPerformanceIndexes1779392970424 } from "./1779392970424-AddPerformanceIndexes";
345
347
  export default [
346
348
  InitialMigration,
347
349
  MigrationName1717678334852,
@@ -687,4 +689,6 @@ export default [
687
689
  AddTelemetryRetentionToHostDockerKubernetes1779282769946,
688
690
  AttachKubernetesAndDockerToIncidentAndAlert1779302536475,
689
691
  AttachServiceToIncidentAndAlert1779303924241,
692
+ AddAgentVersionToKubernetesDockerHost1779392865146,
693
+ AddPerformanceIndexes1779392970424,
690
694
  ];
@@ -82,12 +82,9 @@ export default class Database {
82
82
 
83
83
  @CaptureSpan()
84
84
  public static async checkConnnectionStatus(): Promise<boolean> {
85
- // check popstgres connection to see if it is still alive
86
-
85
+ // SELECT 1 round-trips a connection without scanning any user table.
87
86
  try {
88
- const result: any = await this.dataSource?.query(
89
- `SELECT COUNT(domain) FROM "AcmeChallenge"`,
90
- ); // this is a dummy query to check if the connection is still alive
87
+ const result: any = await this.dataSource?.query(`SELECT 1`);
91
88
 
92
89
  if (!result) {
93
90
  return false;
@@ -1,20 +1,17 @@
1
1
  import ApiKeyService from "../Services/ApiKeyService";
2
2
  import GlobalConfigService from "../Services/GlobalConfigService";
3
3
  import UserService from "../Services/UserService";
4
- import QueryHelper from "../Types/Database/QueryHelper";
5
4
  import {
6
5
  ExpressRequest,
7
6
  ExpressResponse,
8
7
  NextFunction,
9
8
  OneUptimeRequest,
10
9
  } from "../Utils/Express";
11
- import OneUptimeDate from "../../Types/Date";
12
10
  import Dictionary from "../../Types/Dictionary";
13
11
  import BadDataException from "../../Types/Exception/BadDataException";
14
12
  import ObjectID from "../../Types/ObjectID";
15
13
  import { UserTenantAccessPermission } from "../../Types/Permission";
16
14
  import UserType from "../../Types/UserType";
17
- import ApiKey from "../../Models/DatabaseModels/ApiKey";
18
15
  import GlobalConfig from "../../Models/DatabaseModels/GlobalConfig";
19
16
  import User from "../../Models/DatabaseModels/User";
20
17
  import APIKeyAccessPermission from "../Utils/APIKey/AccessPermission";
@@ -89,62 +86,43 @@ export default class ProjectMiddleware {
89
86
  );
90
87
  }
91
88
 
92
- let apiKeyModel: ApiKey | null = null;
89
+ /*
90
+ * Cached lookup — see ApiKeyService.findApiKey. Hot path for any
91
+ * automated caller hitting the API by key.
92
+ */
93
+ const apiKeyRow: { id: ObjectID; projectId: ObjectID } | null =
94
+ await ApiKeyService.findApiKey(apiKey);
93
95
 
94
- if (apiKey) {
95
- apiKeyModel = await ApiKeyService.findOneBy({
96
- query: {
97
- apiKey: apiKey,
98
- expiresAt: QueryHelper.greaterThan(OneUptimeDate.getCurrentDate()),
99
- },
100
- select: {
101
- _id: true,
102
- projectId: true,
103
- },
104
- props: { isRoot: true },
105
- });
96
+ if (apiKeyRow) {
97
+ tenantId = apiKeyRow.projectId;
106
98
 
107
- if (apiKeyModel) {
108
- tenantId = apiKeyModel?.projectId || null;
109
-
110
- if (!tenantId) {
111
- throw new BadDataException("Invalid API Key");
112
- }
99
+ (req as OneUptimeRequest).tenantId = tenantId;
100
+ (req as OneUptimeRequest).userType = UserType.API;
101
+
102
+ /*
103
+ * TODO: Add API key permissions.
104
+ */
105
+ (req as OneUptimeRequest).userGlobalAccessPermission =
106
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(tenantId);
107
+
108
+ const userTenantAccessPermission: UserTenantAccessPermission | null =
109
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
110
+ tenantId,
111
+ apiKeyRow.id,
112
+ );
113
+
114
+ if (userTenantAccessPermission) {
115
+ (req as OneUptimeRequest).userTenantAccessPermission = {};
116
+ (
117
+ (req as OneUptimeRequest)
118
+ .userTenantAccessPermission as Dictionary<UserTenantAccessPermission>
119
+ )[tenantId.toString()] = userTenantAccessPermission;
113
120
 
114
- (req as OneUptimeRequest).tenantId = tenantId;
115
-
116
- if (apiKeyModel) {
117
- (req as OneUptimeRequest).userType = UserType.API;
118
- /*
119
- * TODO: Add API key permissions.
120
- * (req as OneUptimeRequest).permissions =
121
- * apiKeyModel.permissions || [];
122
- */
123
- (req as OneUptimeRequest).userGlobalAccessPermission =
124
- await APIKeyAccessPermission.getDefaultApiGlobalPermission(
125
- tenantId,
126
- );
127
-
128
- const userTenantAccessPermission: UserTenantAccessPermission | null =
129
- await APIKeyAccessPermission.getApiTenantAccessPermission(
130
- tenantId,
131
- apiKeyModel.id!,
132
- );
133
-
134
- if (userTenantAccessPermission) {
135
- (req as OneUptimeRequest).userTenantAccessPermission = {};
136
- (
137
- (req as OneUptimeRequest)
138
- .userTenantAccessPermission as Dictionary<UserTenantAccessPermission>
139
- )[tenantId.toString()] = userTenantAccessPermission;
140
-
141
- return next();
142
- }
143
- }
121
+ return next();
144
122
  }
145
123
  }
146
124
 
147
- if (!apiKeyModel) {
125
+ if (!apiKeyRow) {
148
126
  // check master key.
149
127
  const masterKeyGlobalConfig: GlobalConfig | null =
150
128
  await GlobalConfigService.findOneBy({
@@ -17,7 +17,6 @@ import Response from "../Utils/Response";
17
17
  import ProjectMiddleware from "./ProjectAuthorization";
18
18
  import SpanUtil from "../Utils/Telemetry/SpanUtil";
19
19
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
20
- import OneUptimeDate from "../../Types/Date";
21
20
  import Dictionary from "../../Types/Dictionary";
22
21
  import Exception from "../../Types/Exception/Exception";
23
22
  import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
@@ -201,8 +200,12 @@ export default class UserMiddleware {
201
200
  if (tenantId) {
202
201
  oneuptimeRequest.tenantId = tenantId;
203
202
 
204
- // update last active of project
205
- await ProjectService.updateLastActive(tenantId);
203
+ /*
204
+ * Fire-and-forget: lastActive write is debounced inside the service
205
+ * (60s in-process cache) and we don't need the result before
206
+ * continuing.
207
+ */
208
+ void ProjectService.updateLastActive(tenantId);
206
209
  }
207
210
 
208
211
  if (ProjectMiddleware.hasApiKey(req)) {
@@ -253,31 +256,50 @@ export default class UserMiddleware {
253
256
  : {}),
254
257
  });
255
258
 
256
- await UserService.updateOneBy({
257
- query: {
258
- _id: userId,
259
- },
260
- props: { isRoot: true },
261
- data: { lastActive: OneUptimeDate.getCurrentDate() },
262
- });
259
+ /*
260
+ * Fire-and-forget: lastActive write is debounced inside the service
261
+ * (60s in-process cache) and we don't need the result before continuing.
262
+ */
263
+ void UserService.updateLastActive(new ObjectID(userId));
263
264
 
264
- const userGlobalAccessPermission: UserGlobalAccessPermission | null =
265
- await AccessTokenService.getUserGlobalAccessPermission(
265
+ /*
266
+ * Resolve global permission, tenant permission, and team membership in
267
+ * parallel. These were previously sequential awaits — each added an
268
+ * extra round-trip latency to every authenticated request. The original
269
+ * code wrapped only the tenant-side calls in try/catch (to convert
270
+ * SsoAuthorizationException etc. into an HTTP error response); we
271
+ * preserve that semantic by routing the rejection through the same
272
+ * catch only when tenantId is present.
273
+ */
274
+ const userGlobalAccessPermissionPromise: Promise<UserGlobalAccessPermission | null> =
275
+ AccessTokenService.getUserGlobalAccessPermission(
266
276
  oneuptimeRequest.userAuthorization.userId,
267
277
  );
268
278
 
269
- if (userGlobalAccessPermission) {
270
- oneuptimeRequest.userGlobalAccessPermission = userGlobalAccessPermission;
271
- }
279
+ let userGlobalAccessPermission: UserGlobalAccessPermission | null = null;
272
280
 
273
281
  if (tenantId) {
274
282
  try {
275
- const userTenantAccessPermission: UserTenantAccessPermission | null =
276
- await UserMiddleware.getUserTenantAccessPermissionWithTenantId({
283
+ const [globalPermission, userTenantAccessPermission, userTeamIds]: [
284
+ UserGlobalAccessPermission | null,
285
+ UserTenantAccessPermission | null,
286
+ Array<ObjectID>,
287
+ ] = await Promise.all([
288
+ userGlobalAccessPermissionPromise,
289
+ UserMiddleware.getUserTenantAccessPermissionWithTenantId({
277
290
  req,
278
291
  tenantId,
279
292
  userId: new ObjectID(userId),
280
- });
293
+ }),
294
+ TeamMemberService.getTeamIdsForUser(new ObjectID(userId), tenantId),
295
+ ]);
296
+
297
+ userGlobalAccessPermission = globalPermission;
298
+
299
+ if (userGlobalAccessPermission) {
300
+ oneuptimeRequest.userGlobalAccessPermission =
301
+ userGlobalAccessPermission;
302
+ }
281
303
 
282
304
  if (userTenantAccessPermission) {
283
305
  oneuptimeRequest.userTenantAccessPermission = {};
@@ -291,14 +313,17 @@ export default class UserMiddleware {
291
313
  * an extra DB roundtrip on every permission check. Absent for non-user
292
314
  * callers (API keys, Probes); `Owned` then evaluates as `All`.
293
315
  */
294
- oneuptimeRequest.userTeamIds =
295
- await TeamMemberService.getTeamIdsForUser(
296
- new ObjectID(userId),
297
- tenantId,
298
- );
316
+ oneuptimeRequest.userTeamIds = userTeamIds;
299
317
  } catch (error) {
300
318
  return Response.sendErrorResponse(req, res, error as Exception);
301
319
  }
320
+ } else {
321
+ userGlobalAccessPermission = await userGlobalAccessPermissionPromise;
322
+
323
+ if (userGlobalAccessPermission) {
324
+ oneuptimeRequest.userGlobalAccessPermission =
325
+ userGlobalAccessPermission;
326
+ }
302
327
  }
303
328
 
304
329
  if (req.headers["is-multi-tenant-query"]) {
@@ -468,32 +493,36 @@ export default class UserMiddleware {
468
493
  }): Promise<UserTenantAccessPermission | null> {
469
494
  const { req, tenantId, userId } = data;
470
495
 
471
- const project: Project | null = await ProjectService.findOneById({
472
- id: tenantId,
473
- select: {
474
- requireSsoForLogin: true,
475
- },
476
- props: {
477
- isRoot: true,
478
- },
479
- });
480
-
481
- if (!project) {
482
- throw new TenantNotFoundException("Invalid tenantId");
483
- }
496
+ /*
497
+ * Resolve the SSO requirement and the tenant permission in parallel.
498
+ * `getRequireSsoForLogin` is cached in-process for 60s, so this is
499
+ * usually free; the tenant permission lookup is the expensive call.
500
+ */
501
+ const [requireSsoForLogin, tenantPermission]: [
502
+ boolean,
503
+ UserTenantAccessPermission | null,
504
+ ] = await Promise.all([
505
+ ProjectService.getRequireSsoForLogin(tenantId).catch((err: Error) => {
506
+ /*
507
+ * Preserve the original behavior of throwing a TenantNotFoundException
508
+ * for an unknown project. Any other error re-throws.
509
+ */
510
+ if (err.message === "Project not found") {
511
+ throw new TenantNotFoundException("Invalid tenantId");
512
+ }
513
+ throw err;
514
+ }),
515
+ AccessTokenService.getUserTenantAccessPermission(userId, tenantId),
516
+ ]);
484
517
 
485
518
  if (
486
- project.requireSsoForLogin &&
519
+ requireSsoForLogin &&
487
520
  !UserMiddleware.doesSsoTokenForProjectExist(req, tenantId, userId)
488
521
  ) {
489
522
  throw new SsoAuthorizationException();
490
523
  }
491
524
 
492
- // get project level permissions if projectid exists in request.
493
- return await AccessTokenService.getUserTenantAccessPermission(
494
- userId,
495
- tenantId,
496
- );
525
+ return tenantPermission;
497
526
  }
498
527
 
499
528
  @CaptureSpan()
@@ -524,34 +553,47 @@ export default class UserMiddleware {
524
553
  },
525
554
  });
526
555
 
527
- let result: Dictionary<UserTenantAccessPermission> | null = null;
528
- for (const projectId of projectIds) {
529
- // check if the force sso login is required. and if it is, then check then token.
530
-
531
- let userTenantAccessPermission: UserTenantAccessPermission | null;
532
- if (
533
- projects.find((p: Project) => {
534
- return p._id === projectId.toString() && p.requireSsoForLogin;
535
- }) &&
536
- !UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId)
537
- ) {
538
- // Add default permissions.
539
- userTenantAccessPermission =
540
- UserPermissionUtil.getDefaultUserTenantAccessPermission(projectId);
541
- } else {
542
- // get project level permissions if projectid exists in request.
543
- userTenantAccessPermission =
544
- await AccessTokenService.getUserTenantAccessPermission(
556
+ /*
557
+ * Resolve permissions for every project in parallel. With the previous
558
+ * for-await loop this scaled linearly with project count, adding one
559
+ * round-trip per project even on cache hits.
560
+ */
561
+ const resolved: Array<{
562
+ projectId: ObjectID;
563
+ permission: UserTenantAccessPermission | null;
564
+ }> = await Promise.all(
565
+ projectIds.map(async (projectId: ObjectID) => {
566
+ if (
567
+ projects.find((p: Project) => {
568
+ return p._id === projectId.toString() && p.requireSsoForLogin;
569
+ }) &&
570
+ !UserMiddleware.doesSsoTokenForProjectExist(req, projectId, userId)
571
+ ) {
572
+ return {
573
+ projectId,
574
+ permission:
575
+ UserPermissionUtil.getDefaultUserTenantAccessPermission(
576
+ projectId,
577
+ ),
578
+ };
579
+ }
580
+ return {
581
+ projectId,
582
+ permission: await AccessTokenService.getUserTenantAccessPermission(
545
583
  userId,
546
584
  projectId,
547
- );
548
- }
585
+ ),
586
+ };
587
+ }),
588
+ );
549
589
 
550
- if (userTenantAccessPermission) {
590
+ let result: Dictionary<UserTenantAccessPermission> | null = null;
591
+ for (const { projectId, permission } of resolved) {
592
+ if (permission) {
551
593
  if (!result) {
552
594
  result = {};
553
595
  }
554
- result[projectId.toString()] = userTenantAccessPermission;
596
+ result[projectId.toString()] = permission;
555
597
  }
556
598
  }
557
599
 
@@ -1,10 +1,41 @@
1
1
  import CreateBy from "../Types/Database/CreateBy";
2
- import { OnCreate } from "../Types/Database/Hooks";
2
+ import DeleteBy from "../Types/Database/DeleteBy";
3
+ import UpdateBy from "../Types/Database/UpdateBy";
4
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
5
  import DatabaseService from "./DatabaseService";
6
+ import OneUptimeDate from "../../Types/Date";
4
7
  import ObjectID from "../../Types/ObjectID";
8
+ import QueryHelper from "../Types/Database/QueryHelper";
5
9
  import Model from "../../Models/DatabaseModels/ApiKey";
6
10
  import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
11
+ import InMemoryTTLCache from "../Infrastructure/InMemoryTTLCache";
12
+
13
+ /*
14
+ * 60s is the worst-case staleness on any single API node after a key is
15
+ * revoked from the dashboard. We invalidate in-process immediately on
16
+ * delete/update; this TTL is the upper bound for *other* processes.
17
+ */
18
+ const POSITIVE_TTL_MS: number = 60 * 1000;
19
+ /*
20
+ * Short TTL on misses so an invalid-key flood can't pin entries in the
21
+ * bounded cache for long while still absorbing repeat hits.
22
+ */
23
+ const NEGATIVE_TTL_MS: number = 10 * 1000;
24
+
25
+ interface CachedApiKey {
26
+ id: string;
27
+ projectId: string;
28
+ }
29
+
7
30
  export class Service extends DatabaseService<Model> {
31
+ /*
32
+ * Cache of `apiKey -> { id, projectId }`. The project-auth middleware hits
33
+ * this on every API-key-authenticated request; without it that's a
34
+ * Postgres findOneBy per request for automated callers.
35
+ */
36
+ private apiKeyCache: InMemoryTTLCache<CachedApiKey | null> =
37
+ new InMemoryTTLCache(10_000);
38
+
8
39
  public constructor() {
9
40
  super(Model);
10
41
  }
@@ -16,6 +47,74 @@ export class Service extends DatabaseService<Model> {
16
47
  createBy.data.apiKey = ObjectID.generate();
17
48
  return { createBy, carryForward: null };
18
49
  }
50
+
51
+ @CaptureSpan()
52
+ protected override async onBeforeUpdate(
53
+ updateBy: UpdateBy<Model>,
54
+ ): Promise<OnUpdate<Model>> {
55
+ /*
56
+ * We don't know which keys are being updated without a query; updates
57
+ * are rare so clearing is cheap.
58
+ */
59
+ this.apiKeyCache.clear();
60
+ return { updateBy, carryForward: null };
61
+ }
62
+
63
+ @CaptureSpan()
64
+ protected override async onBeforeDelete(
65
+ deleteBy: DeleteBy<Model>,
66
+ ): Promise<OnDelete<Model>> {
67
+ this.apiKeyCache.clear();
68
+ return { deleteBy, carryForward: null };
69
+ }
70
+
71
+ /**
72
+ * Resolves an API key string to its row, with a short-lived in-process
73
+ * cache. Returns null for unknown or expired keys (also cached, for a
74
+ * shorter TTL). Use this from the auth middleware hot path instead of
75
+ * calling `findOneBy` directly.
76
+ */
77
+ @CaptureSpan()
78
+ public async findApiKey(
79
+ apiKey: ObjectID,
80
+ ): Promise<{ id: ObjectID; projectId: ObjectID } | null> {
81
+ const cacheKey: string = apiKey.toString();
82
+ const cached: CachedApiKey | null | undefined =
83
+ this.apiKeyCache.get(cacheKey);
84
+ if (cached !== undefined) {
85
+ if (cached === null) {
86
+ return null;
87
+ }
88
+ return {
89
+ id: new ObjectID(cached.id),
90
+ projectId: new ObjectID(cached.projectId),
91
+ };
92
+ }
93
+
94
+ const row: Model | null = await this.findOneBy({
95
+ query: {
96
+ apiKey: apiKey,
97
+ expiresAt: QueryHelper.greaterThan(OneUptimeDate.getCurrentDate()),
98
+ },
99
+ select: {
100
+ _id: true,
101
+ projectId: true,
102
+ },
103
+ props: { isRoot: true },
104
+ });
105
+
106
+ if (!row || !row.id || !row.projectId) {
107
+ this.apiKeyCache.set(cacheKey, null, NEGATIVE_TTL_MS);
108
+ return null;
109
+ }
110
+
111
+ this.apiKeyCache.set(
112
+ cacheKey,
113
+ { id: row.id.toString(), projectId: row.projectId.toString() },
114
+ POSITIVE_TTL_MS,
115
+ );
116
+ return { id: row.id, projectId: row.projectId };
117
+ }
19
118
  }
20
119
 
21
120
  export default new Service();