prow-authority 3.6.3

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 (168) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.dockerignore +11 -0
  3. package/.github/workflows/_deploy.yml +191 -0
  4. package/.github/workflows/deploy-dev.yml +30 -0
  5. package/.github/workflows/deploy-prod.yml +34 -0
  6. package/CLAUDE.md +114 -0
  7. package/Dockerfile +61 -0
  8. package/Dockerfile.dev +24 -0
  9. package/README.md +1 -0
  10. package/package.json +61 -0
  11. package/src/App.ts +40 -0
  12. package/src/helpers/api-error.ts +21 -0
  13. package/src/helpers/authority.ts +216 -0
  14. package/src/helpers/config.ts +4 -0
  15. package/src/helpers/holded.ts +105 -0
  16. package/src/helpers/mysql.ts +15 -0
  17. package/src/helpers/notifications.ts +55 -0
  18. package/src/helpers/random.ts +18 -0
  19. package/src/helpers/s3.ts +43 -0
  20. package/src/helpers/stripe.ts +8 -0
  21. package/src/index.ts +47 -0
  22. package/src/v1/common-types.ts +12 -0
  23. package/src/v1/controllers/admin/accounting-seat-controller.ts +148 -0
  24. package/src/v1/controllers/admin/billing-cycle-controller.ts +171 -0
  25. package/src/v1/controllers/admin/invoice-controller.ts +222 -0
  26. package/src/v1/controllers/admin/license-controller.ts +190 -0
  27. package/src/v1/controllers/admin/license-limit-controller.ts +160 -0
  28. package/src/v1/controllers/admin/notification-controller.ts +115 -0
  29. package/src/v1/controllers/admin/organization-controller.ts +210 -0
  30. package/src/v1/controllers/admin/organization-license-controller.ts +224 -0
  31. package/src/v1/controllers/admin/organization-license-limit-controller.ts +196 -0
  32. package/src/v1/controllers/admin/organization-usage-controller.ts +167 -0
  33. package/src/v1/controllers/admin/payment-method-controller.ts +173 -0
  34. package/src/v1/controllers/admin/policy-controller.ts +138 -0
  35. package/src/v1/controllers/admin/policy-version-controller.ts +143 -0
  36. package/src/v1/controllers/admin/schedule-controller.ts +181 -0
  37. package/src/v1/controllers/admin/service-controller.ts +206 -0
  38. package/src/v1/controllers/admin/service-notification-controller.ts +166 -0
  39. package/src/v1/controllers/admin/user-controller.ts +282 -0
  40. package/src/v1/controllers/admin/user-organization-controller.ts +172 -0
  41. package/src/v1/controllers/admin/user-policy-controller.ts +164 -0
  42. package/src/v1/controllers/auth/@deprecated oauth-controller.ts +191 -0
  43. package/src/v1/controllers/auth/identity-token-controller.ts +286 -0
  44. package/src/v1/controllers/auth/organization-token-controller.ts +277 -0
  45. package/src/v1/controllers/auth/temp-code-controller.ts +60 -0
  46. package/src/v1/controllers/billing/billing-controller.ts +805 -0
  47. package/src/v1/controllers/organizations/public-organization-controller.ts +1796 -0
  48. package/src/v1/controllers/policies/public-policy-controller.ts +68 -0
  49. package/src/v1/controllers/services/public-service-controller.ts +177 -0
  50. package/src/v1/controllers/services/service-controller.ts +474 -0
  51. package/src/v1/controllers/users/public-user-controller.ts +471 -0
  52. package/src/v1/daos/@deprecated auth-dao.ts +173 -0
  53. package/src/v1/daos/accounting-seat-dao.ts +111 -0
  54. package/src/v1/daos/billing-cycle-dao.ts +107 -0
  55. package/src/v1/daos/invoice-dao.ts +131 -0
  56. package/src/v1/daos/license-dao.ts +152 -0
  57. package/src/v1/daos/license-limit-dao.ts +120 -0
  58. package/src/v1/daos/organization-dao.ts +109 -0
  59. package/src/v1/daos/organization-license-dao.ts +117 -0
  60. package/src/v1/daos/organization-license-limit-dao.ts +157 -0
  61. package/src/v1/daos/organization-usage-dao.ts +115 -0
  62. package/src/v1/daos/payment-method-dao.ts +110 -0
  63. package/src/v1/daos/policy-dao.ts +105 -0
  64. package/src/v1/daos/policy-version-dao.ts +112 -0
  65. package/src/v1/daos/redis-dao.ts +38 -0
  66. package/src/v1/daos/schema.sql +462 -0
  67. package/src/v1/daos/service-dao.ts +118 -0
  68. package/src/v1/daos/service-notification-dao.ts +120 -0
  69. package/src/v1/daos/user-dao.ts +127 -0
  70. package/src/v1/daos/user-organization-dao.ts +123 -0
  71. package/src/v1/daos/user-policy-dao.ts +109 -0
  72. package/src/v1/middlewares/auth/identity-token-middleware.ts +51 -0
  73. package/src/v1/middlewares/auth/organization-token-middleware.ts +73 -0
  74. package/src/v1/middlewares/auth/service-token-middleware.ts +167 -0
  75. package/src/v1/middlewares/authority-middleware.ts +97 -0
  76. package/src/v1/middlewares/organizations/organization-middleware.ts +94 -0
  77. package/src/v1/middlewares/static/static-middleware.ts +41 -0
  78. package/src/v1/middlewares/users/user-middleware.ts +41 -0
  79. package/src/v1/routes/admin/accounting-seat-router.ts +89 -0
  80. package/src/v1/routes/admin/admin-router.ts +61 -0
  81. package/src/v1/routes/admin/billing-cycle-router.ts +89 -0
  82. package/src/v1/routes/admin/invoice-router.ts +89 -0
  83. package/src/v1/routes/admin/license-limit-router.ts +89 -0
  84. package/src/v1/routes/admin/license-router.ts +89 -0
  85. package/src/v1/routes/admin/notification-router.ts +61 -0
  86. package/src/v1/routes/admin/organization-license-limit-router.ts +89 -0
  87. package/src/v1/routes/admin/organization-license-router.ts +89 -0
  88. package/src/v1/routes/admin/organization-router.ts +89 -0
  89. package/src/v1/routes/admin/payment-method-router.ts +89 -0
  90. package/src/v1/routes/admin/policy-router.ts +89 -0
  91. package/src/v1/routes/admin/policy-version-router.ts +89 -0
  92. package/src/v1/routes/admin/schedule-router.ts +75 -0
  93. package/src/v1/routes/admin/service-notification-router.ts +98 -0
  94. package/src/v1/routes/admin/service-router.ts +89 -0
  95. package/src/v1/routes/admin/user-organization-router.ts +89 -0
  96. package/src/v1/routes/admin/user-policy-router.ts +89 -0
  97. package/src/v1/routes/admin/user-router.ts +89 -0
  98. package/src/v1/routes/auth/@deprecated auth-router.ts +199 -0
  99. package/src/v1/routes/auth/identity-token-router.ts +147 -0
  100. package/src/v1/routes/auth/organization-token-router.ts +70 -0
  101. package/src/v1/routes/billing/billing-router.ts +227 -0
  102. package/src/v1/routes/organizations/invoices-router.ts +48 -0
  103. package/src/v1/routes/organizations/payments-router.ts +104 -0
  104. package/src/v1/routes/organizations/public-organization-router.ts +319 -0
  105. package/src/v1/routes/organizations/users-router.ts +139 -0
  106. package/src/v1/routes/policies/public-policy-router.ts +50 -0
  107. package/src/v1/routes/services/public-service-router.ts +89 -0
  108. package/src/v1/routes/services/service-router.ts +197 -0
  109. package/src/v1/routes/users/public-user-router.ts +230 -0
  110. package/src/v1/routes/v1-router.ts +50 -0
  111. package/src/v1/schemas/admin/accounting-seat-schemas.ts +22 -0
  112. package/src/v1/schemas/admin/billing-cycle-schemas.ts +20 -0
  113. package/src/v1/schemas/admin/invoice-schemas.ts +41 -0
  114. package/src/v1/schemas/admin/license-limit-schemas.ts +25 -0
  115. package/src/v1/schemas/admin/license-schemas.ts +30 -0
  116. package/src/v1/schemas/admin/organization-license-limit-schemas.ts +29 -0
  117. package/src/v1/schemas/admin/organization-license-schemas.ts +43 -0
  118. package/src/v1/schemas/admin/organization-schemas.ts +67 -0
  119. package/src/v1/schemas/admin/organization-usage-schemas.ts +15 -0
  120. package/src/v1/schemas/admin/payment-method-schemas.ts +40 -0
  121. package/src/v1/schemas/admin/policy-schemas.ts +31 -0
  122. package/src/v1/schemas/admin/service-notification-schemas.ts +23 -0
  123. package/src/v1/schemas/admin/service-schemas.ts +56 -0
  124. package/src/v1/schemas/admin/user-organization-schemas.ts +24 -0
  125. package/src/v1/schemas/admin/user-policy-schemas.ts +17 -0
  126. package/src/v1/schemas/admin/user-schemas.ts +52 -0
  127. package/src/v1/schemas/auth/auth-schemas.ts +55 -0
  128. package/src/v1/schemas/billing/billing-schemas.ts +32 -0
  129. package/src/v1/schemas/organizations/public-organization-schemas.ts +77 -0
  130. package/src/v1/schemas/payments/payment-method-schemas.ts +0 -0
  131. package/src/v1/schemas/services/public-service-schemas.ts +5 -0
  132. package/src/v1/schemas/services/service-schemas.ts +27 -0
  133. package/src/v1/schemas/users/public-user-schemas.ts +73 -0
  134. package/src/v1/types/admin/accounting-seat-types.ts +16 -0
  135. package/src/v1/types/admin/billing-cycle-types.ts +14 -0
  136. package/src/v1/types/admin/invoice-types.ts +39 -0
  137. package/src/v1/types/admin/license-limit-types.ts +16 -0
  138. package/src/v1/types/admin/license-types.ts +29 -0
  139. package/src/v1/types/admin/organization-license-limit-types.ts +13 -0
  140. package/src/v1/types/admin/organization-license-types.ts +21 -0
  141. package/src/v1/types/admin/organization-types.ts +60 -0
  142. package/src/v1/types/admin/organization-usage-types.ts +17 -0
  143. package/src/v1/types/admin/payment-method-types.ts +30 -0
  144. package/src/v1/types/admin/policy-types.ts +9 -0
  145. package/src/v1/types/admin/policy-version-types.ts +12 -0
  146. package/src/v1/types/admin/service-notification-types.ts +20 -0
  147. package/src/v1/types/admin/service-types.ts +36 -0
  148. package/src/v1/types/admin/user-organization-types.ts +31 -0
  149. package/src/v1/types/admin/user-types.ts +44 -0
  150. package/src/v1/types/auth/@deprecated auth-types.ts +26 -0
  151. package/src/v1/types/auth/common-types.ts +7 -0
  152. package/src/v1/types/auth/confirmation-types.ts +5 -0
  153. package/src/v1/types/auth/identity-token-types.ts +8 -0
  154. package/src/v1/types/auth/organization-token-types.ts +23 -0
  155. package/src/v1/types/auth/otp-types.ts +12 -0
  156. package/src/v1/types/billing/billing-types.ts +37 -0
  157. package/src/v1/types/billing/ue-types.ts +29 -0
  158. package/src/v1/types/organizations/public-organization-types.ts +76 -0
  159. package/src/v1/types/policies/public-policy-types.ts +7 -0
  160. package/src/v1/types/services/public-service-types.ts +20 -0
  161. package/src/v1/types/services/service-types.ts +43 -0
  162. package/src/v1/types/users/public-user-types.ts +15 -0
  163. package/src/v1/types/users/user-policy-types.ts +11 -0
  164. package/static/error.html +55 -0
  165. package/static/grant.html +117 -0
  166. package/static/login.html +54 -0
  167. package/static/oauth-client-logo.webp +0 -0
  168. package/tsconfig.json +20 -0
@@ -0,0 +1,474 @@
1
+ import { Blocker } from "@beseif-solutions/utility-functions/dist/utils/concurrency-utils";
2
+ import { Date2, Formats, Unit } from "@beseif-solutions/utility-functions/dist/utils/date-utils";
3
+ import { JSON2 } from "@beseif-solutions/utility-functions/dist/utils/json-utils";
4
+ import { Where } from "@beseif-solutions/utility-functions/dist/utils/sql-utils";
5
+ import { DeepPartial } from "@beseif-solutions/utility-functions/dist/utils/typing-utils";
6
+ import { Validator } from "@beseif-solutions/utility-functions/dist/utils/validation-utils";
7
+ import _ from "lodash";
8
+ import APIError, { StatusCodes } from "../../../helpers/api-error";
9
+ import authority, { LicenseLimitApproaching, LicenseLimitReached, NotificationEventNames, ServiceAccessBlocked, ServiceAccessWarning } from "../../../helpers/authority";
10
+ import nconf from "../../../helpers/config";
11
+ import notifications from "../../../helpers/notifications";
12
+ import { quotaParamsSchema, quotaUsageSchema, sendNotificationSchema } from "../../schemas/services/service-schemas";
13
+ import { LimitNotification, OrganizationStatus, ServiceInformation } from "../../types/admin/organization-types";
14
+ import { OrganizationUsage } from "../../types/admin/organization-usage-types";
15
+ import { ServiceNotification } from "../../types/admin/service-notification-types";
16
+ import { UserOrganization, UserOrganizationRole } from "../../types/admin/user-organization-types";
17
+ import { UserStatus } from "../../types/admin/user-types";
18
+ import { BearerAuthorization } from "../../types/auth/common-types";
19
+ import { PublicOrganization, PublicOrganizationLicense } from "../../types/organizations/public-organization-types";
20
+ import { NotificationContext, Quota, QuotaParams, QuotaUsage, SendNotificationData, ServiceEnvironment } from "../../types/services/service-types";
21
+ import { PublicUser } from "../../types/users/public-user-types";
22
+ import organizationController from "../admin/organization-controller";
23
+ import organizationLicenseLimitController from "../admin/organization-license-limit-controller";
24
+ import organizationUsageController from "../admin/organization-usage-controller";
25
+ import serviceNotificationController from "../admin/service-notification-controller";
26
+ import userController from "../admin/user-controller";
27
+ import userOrganizationController from "../admin/user-organization-controller";
28
+ import organizationTokenController from "../auth/organization-token-controller";
29
+ import billingController from "../billing/billing-controller";
30
+ import publicOrganizationController from "../organizations/public-organization-controller";
31
+ import publicUserController from "../users/public-user-controller";
32
+ import { LicenseLimit } from "../../types/admin/license-limit-types";
33
+ import { BillingCycle } from "../../types/admin/billing-cycle-types";
34
+
35
+ class ServiceController {
36
+
37
+
38
+ private readonly blocker = new Blocker<[organization: string, service: string, limit: string]>();
39
+ constructor() {
40
+ this.blocker.getKey = async (organization, service, limit) => `${organization}:${service}:${limit}`;
41
+ }
42
+
43
+ // public information for services
44
+ public readonly getOrganizationByUUID = async (organizationUUID: string): Promise<PublicOrganization> => {
45
+ try {
46
+ const organization = (await organizationController.list({ uuid: organizationUUID, status: { is: OrganizationStatus.Blocked, inverse: true } }, { timestamp: true }))[0];
47
+ if (!organization) {
48
+ throw new APIError({
49
+ code: StatusCodes.NOT_FOUND,
50
+ detail: `Organization ${organizationUUID} not found.`,
51
+ });
52
+ }
53
+ return (await publicOrganizationController.parse(null, [organization]))[0];
54
+ } catch (e) { throw e; }
55
+ };
56
+
57
+ public readonly getUserByUUID = async (userUUID: string): Promise<PublicUser> => {
58
+ try {
59
+ const user = (await userController.list({ uuid: userUUID, status: UserStatus.Active }, { password: false, timestamp: true }))[0];
60
+ if (!user) {
61
+ throw new APIError({
62
+ code: StatusCodes.NOT_FOUND,
63
+ detail: `User ${userUUID} not found.`,
64
+ });
65
+ }
66
+ return (await publicUserController.parseUser([user]))[0];
67
+ } catch (e) { throw e; }
68
+ };
69
+
70
+ // service as organization
71
+ public readonly authorize = async (env: ServiceEnvironment): Promise<BearerAuthorization> => {
72
+ try {
73
+ return organizationTokenController.authorize(env.organization);
74
+ } catch (e) { throw e; }
75
+ };
76
+
77
+ public readonly getOrganization = async (env: ServiceEnvironment): Promise<PublicOrganization> => {
78
+ try {
79
+ const response = (await publicOrganizationController.parse(null, [env.organization]))[0];
80
+ return response;
81
+ } catch (e) { throw e; }
82
+ };
83
+
84
+ public readonly getLicenses = async (env: ServiceEnvironment): Promise<PublicOrganizationLicense[]> => {
85
+ try {
86
+ const response = await publicOrganizationController.parseLicenses(env.organization, env.licenses);
87
+ return response;
88
+ } catch (e) { throw e; }
89
+ };
90
+
91
+ public readonly getQuota = async (env: ServiceEnvironment, params: QuotaParams): Promise<Quota[]> => {
92
+ try {
93
+ Validator.validate(quotaParamsSchema, params);
94
+
95
+ // get limitations
96
+ const limits = await organizationLicenseLimitController.list({
97
+ license: { in: env.licenses.map((l) => l.id) },
98
+ ...params.name ? { name: { in: params.name } } : {},
99
+ });
100
+
101
+ // get usages
102
+ const queries: Where<OrganizationUsage>[] = [];
103
+
104
+ const { cycle, now } = await billingController.cycleContext(env.organization, params.date);
105
+
106
+ const byPeriod = _.groupBy(limits, (l) => l.period);
107
+
108
+ if (byPeriod[`monthly`]) {
109
+ const monthlyScope = this.getLimitScope(`monthly`, cycle, now);
110
+
111
+ queries.push({
112
+ service: env.service.id,
113
+ organization: env.organization.id,
114
+ name: { in: byPeriod[`monthly`].map((l) => l.name) },
115
+ date: { gte: monthlyScope.start as any, lt: monthlyScope.end as any },
116
+ });
117
+ }
118
+
119
+ if (byPeriod[`lifetime`]) {
120
+ queries.push({
121
+ service: env.service.id,
122
+ organization: env.organization.id,
123
+ name: { in: byPeriod[`lifetime`].map((l) => l.name) },
124
+ });
125
+ }
126
+
127
+ const usages = queries.length > 0 ? await organizationUsageController.list(queries) : [];
128
+
129
+ // parse
130
+ const response: Quota[] = _.map(_.groupBy(limits, (l) => l.name), ((sameLimits, name): Quota => {
131
+
132
+ const period = sameLimits[0].period;
133
+ const scope = this.getLimitScope(period, cycle, now);
134
+
135
+ return {
136
+ period: period,
137
+ name: name,
138
+ overconsume: !cycle.trial && sameLimits.some((l) => l.overconsume),
139
+ limit: _.sum(sameLimits.map((sl) => sl.limit)),
140
+ used: _.sum(usages.filter((u) => u.name === name).map((u) => u.count)),
141
+ ...scope ? { resets: scope.end.format(Formats.ISO) } : {},
142
+ };
143
+ }));
144
+ return response;
145
+ } catch (e) { throw e; }
146
+ };
147
+
148
+ public readonly checkQuota = async (env: ServiceEnvironment): Promise<void> => {
149
+ try {
150
+ const exceededLimits = (await serviceController.getQuota(env, {}))
151
+ .filter((q) => q.used > q.limit && q.period === `lifetime` && !q.overconsume);
152
+
153
+ if (exceededLimits.length > 0) {
154
+ const information = await this.getOrganizationServiceInformation(env);
155
+
156
+ // register when a limit was exceeded
157
+ if (!information.exceeded_date) { information.exceeded_date = Date2.current().format(Formats.ISO); }
158
+
159
+ if (!information.blocked) {
160
+ // get owner
161
+ const access = (await userOrganizationController.list({ organization: env.organization.id, role: UserOrganizationRole.Owner }))[0];
162
+ if (!access) { throw new Error(`No owner found for organization ${env.organization.properties.name}`); }
163
+
164
+ const owner = (await userController.get(access.user, { password: false }))[0];
165
+ if (!owner) { throw new Error(`No owner found for organization ${env.organization.properties.name}`); }
166
+
167
+ const exceeded = new Date2(information.exceeded_date);
168
+ const today = Date2.current();
169
+
170
+ const diffInDays = Math.abs(today.toMoment().diff(exceeded.toMoment(), `days`));
171
+
172
+ const MARGIN = nconf.get(`SERVICES:SCHEDULE:LIMITS:MARGIN`);
173
+
174
+ if (diffInDays < MARGIN) {
175
+ const key = today.format(Formats.SqlDate);
176
+
177
+ if (!information.notifications.access[key]) {
178
+ // send email
179
+ await authority.notify<ServiceAccessWarning>(env.organization.uuid, {
180
+ event: NotificationEventNames.ServiceAccessWarning,
181
+ data: {
182
+ days: MARGIN - diffInDays,
183
+ organization: env.organization,
184
+ cta: {
185
+ usage: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_USAGE`),
186
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
187
+ },
188
+ },
189
+ });
190
+
191
+ // register notification to avoid duplicated emails
192
+ information.notifications.access[key] = true;
193
+ }
194
+ } else {
195
+ // send email
196
+ await authority.notify<ServiceAccessBlocked>(env.organization.uuid, {
197
+ event: NotificationEventNames.ServiceAccessBlocked,
198
+ data: {
199
+ organization: env.organization,
200
+ service: env.service,
201
+ cta: {
202
+ usage: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_USAGE`),
203
+ upgrade: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
204
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:PREFERENCES_URL`),
205
+ },
206
+ },
207
+ });
208
+
209
+ // block access to service
210
+ information.blocked = true;
211
+ }
212
+ }
213
+
214
+ await this.updateOrganizationServiceInformation(env, information);
215
+ } else if (env.organization.internal.services[env.service.uuid]) {
216
+ await this.updateOrganizationServiceInformation(env, {
217
+ blocked: false,
218
+ exceeded_date: null,
219
+ notifications: {
220
+ access: null,
221
+ },
222
+ });
223
+ }
224
+ } catch (e) { throw e; }
225
+ };
226
+
227
+ public readonly validateQuotaUsage = async (env: ServiceEnvironment, data: QuotaUsage): Promise<{ possible: boolean }> => {
228
+ try {
229
+ Validator.validate(quotaUsageSchema, data);
230
+
231
+ const quota = (await this.getQuota(env, { name: [data.name] }))[0];
232
+ if (!quota) {
233
+ throw new APIError({
234
+ code: StatusCodes.BAD_REQUEST,
235
+ detail: `Invalid name for quota usage.`,
236
+ });
237
+ }
238
+
239
+ const futureUsed = quota.used + data.count;
240
+
241
+ return { possible: quota.overconsume || futureUsed <= quota.limit && futureUsed >= 0 };
242
+ } catch (e) { throw e; }
243
+ };
244
+
245
+ public readonly registerQuotaUsage = async (env: ServiceEnvironment, data: QuotaUsage): Promise<void> => {
246
+ try {
247
+ Validator.validate(quotaUsageSchema, data);
248
+
249
+ if (data.count === 0) { return; }
250
+
251
+ const limits = await organizationLicenseLimitController.list({
252
+ license: { in: env.licenses.map((l) => l.id) },
253
+ name: data.name,
254
+ });
255
+ if (!limits.length) {
256
+ throw new APIError({
257
+ code: StatusCodes.BAD_REQUEST,
258
+ detail: `Invalid name for quota usage.`,
259
+ });
260
+ }
261
+
262
+ await organizationUsageController.add({
263
+ service: env.service.id,
264
+ organization: env.organization.id,
265
+ name: data.name,
266
+ count: data.count,
267
+ });
268
+
269
+ // check and notify quota limits (non blocking)
270
+ setImmediate(async () => this.checkAndNotifyQuotaLimits(env, data.name));
271
+ } catch (e) { throw e; }
272
+ };
273
+
274
+ public readonly sendNotification = async (env: ServiceEnvironment, data: SendNotificationData): Promise<{ sent: number }> => {
275
+ try {
276
+ Validator.validate(sendNotificationSchema, data);
277
+
278
+ const notificationConfig = (await serviceNotificationController.list({
279
+ service: env.service.id,
280
+ event: data.event,
281
+ enabled: true,
282
+ }))[0];
283
+ if (!notificationConfig) { return { sent: 0 }; }
284
+
285
+ const organizationUsers = await userOrganizationController.list({
286
+ organization: env.organization.id,
287
+ enabled: true,
288
+ });
289
+
290
+ const recipients: { email: string, language: string }[] = [];
291
+ for (const organizationUser of organizationUsers) {
292
+ const shouldNotify = await this.shouldNotifyUser(notificationConfig, organizationUser, { event: data.event, context: data.context || {} });
293
+
294
+ if (shouldNotify) {
295
+ const recipient = (await userController.get(organizationUser.user, { password: false }))[0];
296
+ if (recipient) { recipients.push({ email: recipient.email, language: recipient.preferences.language }); }
297
+ }
298
+ }
299
+
300
+ if (recipients.length > 0) {
301
+ for (const recipient of recipients) {
302
+ const params = {
303
+ template: notificationConfig.template,
304
+ locale: data.locale || recipient.language,
305
+ to: [recipient.email],
306
+ arguments: {
307
+ event: data.event,
308
+ service: env.service,
309
+ organization: env.organization,
310
+ data: data.data || {},
311
+ },
312
+ ...data.attachments ? { attachments: data.attachments } : {},
313
+ };
314
+
315
+ await notifications.gmail(params);
316
+ }
317
+ }
318
+
319
+ return { sent: recipients.length };
320
+ } catch (e) { throw e; }
321
+ };
322
+
323
+ private readonly checkAndNotifyQuotaLimits = async (env: ServiceEnvironment, name: string): Promise<void> => {
324
+ try {
325
+ await this.blocker.block(env.organization.uuid, env.service.uuid, name);
326
+
327
+ const ALERT_THRESHOLD = nconf.get(`SERVICES:AUTHORITY:LICENSES:ALERT_THRESHOLD`);
328
+ const FULL_THRESHOLD = 100;
329
+
330
+ // get and validate quota
331
+ const quota = (await this.getQuota(env, { name: [name] }))[0];
332
+
333
+ // ignore limits if overconsume is activated or limits don't reset
334
+ if (quota.overconsume || !quota.resets) { return; }
335
+
336
+ const currentPercentage = (quota.used / quota.limit) * FULL_THRESHOLD;
337
+ const information = await this.getOrganizationServiceInformation(env);
338
+ const limitInfo: LimitNotification = _.get(
339
+ information,
340
+ [`notifications`, `limits`, name],
341
+ { approaching: false, reached: false, lastPercentage: undefined }
342
+ );
343
+
344
+ const isReached = currentPercentage >= FULL_THRESHOLD;
345
+ const isApproaching = currentPercentage >= ALERT_THRESHOLD && !isReached;
346
+
347
+ if (isReached && !limitInfo.reached) {
348
+ await authority.notify<LicenseLimitReached>(env.organization.uuid, {
349
+ event: NotificationEventNames.LicenseLimitReached,
350
+ data: {
351
+ limit: quota,
352
+ organization: env.organization,
353
+ cta: {
354
+ upgrade: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
355
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
356
+ },
357
+ },
358
+ });
359
+ } else if (isApproaching && !limitInfo.approaching && !limitInfo.reached) {
360
+ await authority.notify<LicenseLimitApproaching>(env.organization.uuid, {
361
+ event: NotificationEventNames.LicenseLimitApproaching,
362
+ data: {
363
+ limit: {
364
+ ...quota,
365
+ percentage: currentPercentage,
366
+ },
367
+ organization: env.organization,
368
+ cta: {
369
+ upgrade: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:ORG_BILLING`),
370
+ preferences: nconf.get(`SERVICES:NOTIFICATIONS:CTAS:USER_PREF_LIST`),
371
+ },
372
+ },
373
+ });
374
+ }
375
+
376
+ limitInfo.approaching = isApproaching;
377
+ limitInfo.reached = isReached;
378
+ limitInfo.lastPercentage = currentPercentage;
379
+
380
+ await this.updateOrganizationServiceInformation(env, {
381
+ notifications: { limits: { [name]: limitInfo } },
382
+ });
383
+ } catch (e) {
384
+ console.log(`Error while checking and notifying quota limits:`, e);
385
+ } finally {
386
+ await this.blocker.unblock(env.organization.uuid, env.service.uuid, name);
387
+ }
388
+ };
389
+
390
+ private readonly shouldNotifyUser = async (
391
+ config: ServiceNotification,
392
+ organizationUser: UserOrganization,
393
+ data: { event: string, context: NotificationContext }
394
+ ): Promise<boolean> => {
395
+ try {
396
+ const { event, context } = data;
397
+
398
+ // by configuration policy
399
+ if (!config.scope.roles.includes(organizationUser.role)) { return false; }
400
+
401
+ // by preferences
402
+ const eventPreference = _.get(organizationUser, `preferences.notifications.${event}`, config.default);
403
+ if (typeof eventPreference === `boolean`) { return eventPreference; }
404
+
405
+ for (const key of Object.keys(context)) {
406
+ const arr = eventPreference[key] || [];
407
+ if (arr.includes(context[key])) { return true; }
408
+ }
409
+
410
+ return false;
411
+ } catch { return false; }
412
+ };
413
+
414
+ public readonly getLimitScope = (period: LicenseLimit[`period`], cycle: BillingCycle, now: Date2): { start: Date2, end: Date2 } => {
415
+ try {
416
+ const add: Record<LicenseLimit[`period`], Unit> = {
417
+ lifetime: null,
418
+ monthly: Unit.Months,
419
+ };
420
+
421
+ const unit = add[period];
422
+ if (!unit) { return null; }
423
+
424
+ const cycleStart = new Date2(cycle.start_date);
425
+ const cycleEnd = new Date2(cycle.end_date);
426
+ const current = new Date2(now);
427
+
428
+ if (current.isBefore(cycleStart) || current.isAfter(cycleEnd)) { throw new Error(`Current date is outside the billing cycle range.`); }
429
+
430
+ let start = new Date2(cycle.start_date);
431
+ let end = new Date2(cycle.start_date).add(1, unit);
432
+
433
+ while (current.isAfter(end) || current.isEqual(end)) {
434
+ start = start.add(1, unit);
435
+ end = end.add(1, unit);
436
+ }
437
+
438
+ if (end.isAfter(cycleEnd)) { end = cycleEnd; }
439
+
440
+ return { start: start, end: end };
441
+ } catch (e) { throw e; }
442
+ };
443
+
444
+ private readonly getOrganizationServiceInformation = async (env: ServiceEnvironment): Promise<ServiceInformation> => {
445
+ try {
446
+ const org = (await organizationController.get(env.organization.id))[0];
447
+ if (!org) {
448
+ throw new APIError({
449
+ code: StatusCodes.NOT_FOUND,
450
+ detail: `Organization not found.`,
451
+ });
452
+ }
453
+
454
+ const information: ServiceInformation = JSON2.deepMerge({
455
+ blocked: false,
456
+ notifications: {
457
+ access: {},
458
+ limits: {},
459
+ },
460
+ }, _.get(org.internal, `services.${env.service.uuid}`, {}));
461
+ return information;
462
+ } catch (e) { throw e; }
463
+ };
464
+
465
+ private readonly updateOrganizationServiceInformation = async (env: ServiceEnvironment, information: DeepPartial<ServiceInformation>): Promise<void> => {
466
+ try {
467
+ await organizationController.update(env.organization.id, { internal: { services: { [env.service.uuid]: information } } });
468
+ } catch (e) { throw e; }
469
+ };
470
+
471
+ }
472
+
473
+ const serviceController = new ServiceController();
474
+ export default serviceController;