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,206 @@
1
+ import { Date2, Formats } from "@beseif-solutions/utility-functions/dist/utils/date-utils";
2
+ import { JSON2 } from "@beseif-solutions/utility-functions/dist/utils/json-utils";
3
+ import { Where } from "@beseif-solutions/utility-functions/dist/utils/sql-utils";
4
+ import { newUUIDv4 } from "@beseif-solutions/utility-functions/dist/utils/string-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 APIError, { StatusCodes } from "../../../helpers/api-error";
8
+ import { deleteFile, uploadFile } from "../../../helpers/s3";
9
+ import { ALPHANUMERIC_CHARSET, generateRandomString } from "../../../helpers/random";
10
+ import serviceDao from "../../daos/service-dao";
11
+ import { addServiceSchema, updateServiceSchema } from "../../schemas/admin/service-schemas";
12
+ import { DBRecord, Service } from "../../types/admin/service-types";
13
+ import nconf from "../../../helpers/config";
14
+ import { performance } from "perf_hooks";
15
+ import { Modifiers } from "../../common-types";
16
+
17
+ type Add = Omit<Service, `id` | `uuid` | `enabled` | `created_date` | `modified_date`>;
18
+ type Update = DeepPartial<Omit<Service, `id` | `uuid` | `created_date` | `modified_date`>>;
19
+
20
+ class ServiceCtrl {
21
+
22
+ private readonly SERVICE_TOKEN_LENGTH = 100;
23
+
24
+ public readonly get = async (id: number): Promise<Service[]> => {
25
+ try {
26
+ const db = await serviceDao.get({ id: id });
27
+ const response = await this.parse(db);
28
+ return response;
29
+ } catch (e) { throw e; }
30
+ };
31
+
32
+ public readonly list = async (params: Where<Service>, options: Modifiers = {}): Promise<Service[]> => {
33
+ try {
34
+ const db = await serviceDao.list({ ...params }, options);
35
+ const response = await this.parse(db);
36
+ return response;
37
+ } catch (e) {
38
+ throw e;
39
+ }
40
+ };
41
+
42
+ public readonly add = async (data: Add): Promise<Service[]> => {
43
+ try {
44
+ await Validator.validateAsync(addServiceSchema, data);
45
+ await this.check(data);
46
+
47
+ const uuid = await this.getFreeUUID();
48
+
49
+ if (data.properties.image) {
50
+ data.properties.image =
51
+ data.properties.image.startsWith(`http`) ?
52
+ data.properties.image :
53
+ await uploadFile({
54
+ entity: `services`,
55
+ uuid: uuid,
56
+ name: `avatar`,
57
+ }, data.properties.image);
58
+ }
59
+
60
+ const { insertId } = await serviceDao.add({
61
+ uuid: uuid,
62
+ token: generateRandomString(ALPHANUMERIC_CHARSET, nconf.get(`SERVICES:AUTH:SERVICE:LENGTH`)),
63
+ properties: data.properties,
64
+ preferences: data.preferences,
65
+ enabled: true,
66
+ metadata: data.metadata,
67
+ internal: data.internal,
68
+ });
69
+
70
+ return await this.get(insertId);
71
+ } catch (e) { throw e; }
72
+ };
73
+
74
+ public readonly update = async (id: number, data: Update): Promise<Service[]> => {
75
+ try {
76
+ await Validator.validateAsync(updateServiceSchema, data);
77
+
78
+ const old = (await this.get(id))[0];
79
+ if (!old) {
80
+ throw new APIError({
81
+ code: StatusCodes.NOT_FOUND,
82
+ detail: `Service with id ${id} not found.`,
83
+ });
84
+ }
85
+
86
+ const merged = JSON2.deepClean(JSON2.deepMerge({}, old, data)) as Service;
87
+
88
+ await this.check(merged, old);
89
+
90
+ if (data.properties?.image === null && old.properties.image) {
91
+ await deleteFile({
92
+ entity: `services`,
93
+ uuid: merged.uuid,
94
+ name: `avatar`,
95
+ });
96
+ merged.properties.image = null;
97
+ } else if (data.properties?.image) {
98
+ merged.properties.image =
99
+ data.properties.image.startsWith(`http`) ?
100
+ data.properties.image :
101
+ await uploadFile({
102
+ entity: `services`,
103
+ uuid: merged.uuid,
104
+ name: `avatar`,
105
+ }, data.properties.image);
106
+ }
107
+
108
+ if (data.token) { merged.token = generateRandomString(ALPHANUMERIC_CHARSET, nconf.get(`SERVICES:AUTH:SERVICE:LENGTH`)); }
109
+
110
+ const { affectedRows } = await serviceDao.update({ id: id }, {
111
+ uuid: merged.uuid,
112
+ token: merged.token,
113
+ properties: merged.properties,
114
+ preferences: merged.preferences,
115
+ enabled: merged.enabled,
116
+ metadata: merged.metadata,
117
+ internal: merged.internal,
118
+ });
119
+
120
+ if (affectedRows <= 0) {
121
+ throw new APIError({
122
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
123
+ detail: `Could not update service with id ${id}`,
124
+ });
125
+ }
126
+
127
+ return await this.get(id);
128
+ } catch (e) { throw e; }
129
+
130
+ };
131
+
132
+ public readonly delete = async (id: number): Promise<{ deleted: boolean }> => {
133
+ try {
134
+ const service = (await this.get(id))[0];
135
+ if (!service) {
136
+ throw new APIError({
137
+ code: StatusCodes.NOT_FOUND,
138
+ detail: `Service with id ${id} not found.`,
139
+ });
140
+ }
141
+
142
+ const { affectedRows } = await serviceDao.delete({ id: service.id });
143
+
144
+ if (affectedRows <= 0) {
145
+ throw new APIError({
146
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
147
+ detail: `Could not delete service with id ${id}`,
148
+ });
149
+ }
150
+
151
+ return { deleted: affectedRows > 0 };
152
+ } catch (e) { throw e; }
153
+ };
154
+
155
+ private readonly getFreeUUID = async (): Promise<string> => {
156
+ try {
157
+ let uuid: string;
158
+ do {
159
+ uuid = newUUIDv4({ case: `lower` });
160
+ const exists = (await serviceDao.list({ uuid: uuid }))[0];
161
+ if (!exists) { break; }
162
+ } while (true);
163
+ return uuid;
164
+ } catch (e) { throw e; }
165
+ };
166
+
167
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
168
+ private readonly check = async (data: Add | Service, old?: Service): Promise<void> => {
169
+ try {
170
+ // TODO: nothing to check
171
+ } catch (e) { throw e; }
172
+ };
173
+
174
+ private readonly parse = async (records: DBRecord[]): Promise<Service[]> => {
175
+ try {
176
+ const response: Service[] = [];
177
+
178
+ for (const record of records) {
179
+ const p: Service = {
180
+ id: record.id,
181
+ uuid: record.uuid,
182
+ token: record.token,
183
+ properties: {
184
+ name: record.properties.name,
185
+ description: record.properties.description,
186
+ home: record.properties.home,
187
+ image: `${record.properties.image}?ts=${performance.now()}`,
188
+ },
189
+ preferences: record.preferences,
190
+ created_date: new Date2(record.created_date).format(Formats.ISO),
191
+ modified_date: new Date2(record.modified_date).format(Formats.ISO),
192
+ enabled: record.enabled ? true : false,
193
+ metadata: record.metadata,
194
+ internal: record.internal,
195
+ };
196
+
197
+ response.push(p);
198
+ }
199
+
200
+ return response;
201
+ } catch (e) { throw e; }
202
+ };
203
+ }
204
+
205
+ const serviceController = new ServiceCtrl();
206
+ export default serviceController;
@@ -0,0 +1,166 @@
1
+ import { Date2, Formats } from "@beseif-solutions/utility-functions/dist/utils/date-utils";
2
+ import { JSON2 } from "@beseif-solutions/utility-functions/dist/utils/json-utils";
3
+ import { Where } from "@beseif-solutions/utility-functions/dist/utils/sql-utils";
4
+ import { DeepPartial } from "@beseif-solutions/utility-functions/dist/utils/typing-utils";
5
+ import { Validator } from "@beseif-solutions/utility-functions/dist/utils/validation-utils";
6
+ import APIError, { StatusCodes } from "../../../helpers/api-error";
7
+ import serviceNotificationDao from "../../daos/service-notification-dao";
8
+ import { addServiceNotificationSchema, updateServiceNotificationSchema } from "../../schemas/admin/service-notification-schemas";
9
+ import { DBRecord, ServiceNotification } from "../../types/admin/service-notification-types";
10
+ import { Modifiers } from "../../common-types";
11
+
12
+ type Add = Omit<ServiceNotification, `id` | `enabled` | `created_date` | `modified_date`>;
13
+ type Update = DeepPartial<Omit<ServiceNotification, `id` | `created_date` | `modified_date`>>;
14
+
15
+ class ServiceNotificationCtrl {
16
+
17
+ public readonly get = async (id: number): Promise<ServiceNotification[]> => {
18
+ try {
19
+ const db = await serviceNotificationDao.get({ id: id });
20
+ const response = await this.parse(db);
21
+ return response;
22
+ } catch (e) { throw e; }
23
+ };
24
+
25
+ public readonly list = async (params: Where<DBRecord>, options: Modifiers = {}): Promise<ServiceNotification[]> => {
26
+ try {
27
+ const db = await serviceNotificationDao.list({ ...params }, options);
28
+ const response = await this.parse(db);
29
+ return response;
30
+ } catch (e) {
31
+ throw e;
32
+ }
33
+ };
34
+
35
+ public readonly add = async (data: Add): Promise<ServiceNotification[]> => {
36
+ try {
37
+ await Validator.validateAsync(addServiceNotificationSchema, data);
38
+ await this.check(data);
39
+
40
+ const { insertId } = await serviceNotificationDao.add({
41
+ service: data.service,
42
+ event: data.event,
43
+ template: data.template,
44
+ scope: data.scope,
45
+ default: data.default,
46
+ enabled: true,
47
+ metadata: data.metadata,
48
+ });
49
+
50
+ return await this.get(insertId);
51
+ } catch (e) { throw e; }
52
+ };
53
+
54
+ public readonly update = async (id: number, data: Update): Promise<ServiceNotification[]> => {
55
+ try {
56
+ await Validator.validateAsync(updateServiceNotificationSchema, data);
57
+
58
+ const old = (await this.get(id))[0];
59
+ if (!old) {
60
+ throw new APIError({
61
+ code: StatusCodes.NOT_FOUND,
62
+ detail: `Service notification with id ${id} not found.`,
63
+ });
64
+ }
65
+
66
+ const merged = JSON2.deepClean(JSON2.deepMerge({}, old, data)) as ServiceNotification;
67
+
68
+ await this.check(merged, old);
69
+
70
+ const { affectedRows } = await serviceNotificationDao.update({ id: id }, {
71
+ service: merged.service,
72
+ event: merged.event,
73
+ template: merged.template,
74
+ scope: merged.scope,
75
+ default: merged.default,
76
+ enabled: merged.enabled,
77
+ metadata: merged.metadata,
78
+ });
79
+
80
+ if (affectedRows <= 0) {
81
+ throw new APIError({
82
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
83
+ detail: `Could not update service notification with id ${id}`,
84
+ });
85
+ }
86
+
87
+ return await this.get(id);
88
+ } catch (e) { throw e; }
89
+
90
+ };
91
+
92
+ public readonly delete = async (id: number): Promise<{ deleted: boolean }> => {
93
+ try {
94
+ const serviceNotification = (await this.get(id))[0];
95
+ if (!serviceNotification) {
96
+ throw new APIError({
97
+ code: StatusCodes.NOT_FOUND,
98
+ detail: `Service notification with id ${id} not found.`,
99
+ });
100
+ }
101
+
102
+ const { affectedRows } = await serviceNotificationDao.delete({ id: serviceNotification.id });
103
+
104
+ if (affectedRows <= 0) {
105
+ throw new APIError({
106
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
107
+ detail: `Could not delete service notification with id ${id}`,
108
+ });
109
+ }
110
+
111
+ return { deleted: affectedRows > 0 };
112
+ } catch (e) { throw e; }
113
+ };
114
+
115
+ private readonly check = async (data: Add | ServiceNotification, old?: ServiceNotification): Promise<void> => {
116
+ try {
117
+ if (!old || data.service !== old.service || data.event !== old.event) {
118
+ const existing = await this.list({ service: data.service, event: data.event, enabled: true });
119
+ const conflict = existing.find((e) => !old || e.id !== old.id);
120
+ if (conflict) {
121
+ throw new APIError({
122
+ code: StatusCodes.BAD_REQUEST,
123
+ detail: `A service notification for service ${data.service} and event '${data.event}' already exists (id: ${conflict.id}).`,
124
+ });
125
+ }
126
+ }
127
+
128
+ if (data.scope && data.scope.roles) {
129
+ const allowed = [`owner`, `admin`, `staff`];
130
+ if (!Array.isArray(data.scope.roles) || !data.scope.roles.every((r) => allowed.includes(r))) {
131
+ throw new APIError({
132
+ code: StatusCodes.BAD_REQUEST,
133
+ detail: `scope.roles must be a subset of ${JSON.stringify(allowed)}.`,
134
+ });
135
+ }
136
+ }
137
+ } catch (e) { throw e; }
138
+ };
139
+
140
+ private readonly parse = async (records: DBRecord[]): Promise<ServiceNotification[]> => {
141
+ try {
142
+ const response: ServiceNotification[] = [];
143
+
144
+ for (const record of records) {
145
+ const p: ServiceNotification = {
146
+ id: record.id,
147
+ service: record.service,
148
+ event: record.event,
149
+ template: record.template,
150
+ scope: record.scope,
151
+ created_date: new Date2(record.created_date).format(Formats.ISO),
152
+ modified_date: new Date2(record.modified_date).format(Formats.ISO),
153
+ default: record.default ? true : false,
154
+ enabled: record.enabled ? true : false,
155
+ metadata: record.metadata,
156
+ };
157
+ response.push(p);
158
+ }
159
+
160
+ return response;
161
+ } catch (e) { throw e; }
162
+ };
163
+ }
164
+
165
+ const serviceNotificationController = new ServiceNotificationCtrl();
166
+ export default serviceNotificationController;
@@ -0,0 +1,282 @@
1
+ import { Hash } from "@beseif-solutions/utility-functions/dist/utils/crypto-utils";
2
+ import { Date2, Formats } 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 { newUUIDv4 } from "@beseif-solutions/utility-functions/dist/utils/string-utils";
6
+ import { DeepPartial } from "@beseif-solutions/utility-functions/dist/utils/typing-utils";
7
+ import { Validator } from "@beseif-solutions/utility-functions/dist/utils/validation-utils";
8
+ import _ from "lodash";
9
+ import APIError, { StatusCodes } from "../../../helpers/api-error";
10
+ import nconf from "../../../helpers/config";
11
+ import { deleteFile, uploadFile } from "../../../helpers/s3";
12
+ import userDao from "../../daos/user-dao";
13
+ import { addUserSchema, updateUserSchema } from "../../schemas/admin/user-schemas";
14
+ import { UserOrganizationRole } from "../../types/admin/user-organization-types";
15
+ import { DBRecord, User, UserStatus } from "../../types/admin/user-types";
16
+ import identityTokenController from "../auth/identity-token-controller";
17
+ import userOrganizationController from "./user-organization-controller";
18
+ import { performance } from "perf_hooks";
19
+ import { Modifiers } from "../../common-types";
20
+
21
+ type Add = Partial<Omit<User, `id` | `uuid` | `status` | `created_date` | `modified_date`>>;
22
+
23
+ type Update = DeepPartial<Omit<User, `id` | `uuid` | `status` | `created_date` | `modified_date`>>;
24
+
25
+ type Options = { password: boolean, timestamp?: true };
26
+
27
+ class UserCtrl {
28
+
29
+ public readonly get = async (id: number, options: Options): Promise<User[]> => {
30
+ try {
31
+ const db = await userDao.get({ id: id });
32
+ const response = await this.parse(db, options);
33
+ return response;
34
+ } catch (e) { throw e; }
35
+ };
36
+
37
+ public readonly list = async (params: Where<User>, options: Options & Modifiers): Promise<User[]> => {
38
+ try {
39
+ const db = await userDao.list({ ...params }, options);
40
+ const response = await this.parse(db, options);
41
+ return response;
42
+ } catch (e) {
43
+ throw e;
44
+ }
45
+ };
46
+
47
+ public readonly add = async (data: Add, options: Options): Promise<User[]> => {
48
+ try {
49
+ await Validator.validateAsync(addUserSchema, data);
50
+ await this.check(data);
51
+
52
+ const uuid = await this.getFreeUUID();
53
+
54
+ if (data.properties?.image) {
55
+ data.properties.image =
56
+ data.properties.image.startsWith(`http`) ?
57
+ data.properties.image :
58
+ await uploadFile({
59
+ entity: `users`,
60
+ uuid: uuid,
61
+ name: `avatar`,
62
+ }, data.properties.image);
63
+ }
64
+
65
+ const { insertId } = await userDao.add({
66
+ uuid: uuid,
67
+ email: data.email,
68
+ password: data.password ? this.hashPassword(data.password) : null,
69
+ properties: data.properties,
70
+ preferences: data.preferences,
71
+ utm: data.utm,
72
+ referrer: data.referrer,
73
+ omnipotent: data.omnipotent,
74
+ status: UserStatus.Pending,
75
+ metadata: data.metadata,
76
+ });
77
+
78
+ return await this.get(insertId, options);
79
+ } catch (e) { throw e; }
80
+ };
81
+
82
+ public readonly update = async (id: number, data: Update, options: Options): Promise<User[]> => {
83
+ try {
84
+ await Validator.validateAsync(updateUserSchema, data);
85
+
86
+ const old = (await this.get(id, { password: true }))[0];
87
+ if (!old) {
88
+ throw new APIError({
89
+ code: StatusCodes.NOT_FOUND,
90
+ detail: `User with id ${id} not found.`,
91
+ });
92
+ }
93
+
94
+ const merged = JSON2.deepClean(JSON2.deepMerge({}, old, data)) as User;
95
+
96
+ await this.check(merged, old);
97
+
98
+ if (data.properties?.image === null && old.properties.image) {
99
+ await deleteFile({
100
+ entity: `users`,
101
+ uuid: merged.uuid,
102
+ name: `avatar`,
103
+ });
104
+ merged.properties.image = null;
105
+ } else if (data.properties?.image) {
106
+ merged.properties.image =
107
+ data.properties.image.startsWith(`http`) ?
108
+ data.properties.image :
109
+ await uploadFile({
110
+ entity: `users`,
111
+ uuid: merged.uuid,
112
+ name: `avatar`,
113
+ }, data.properties.image);
114
+ }
115
+
116
+ const { affectedRows } = await userDao.update({ id: id }, {
117
+ uuid: merged.uuid,
118
+ email: merged.email,
119
+ password: merged.password,
120
+ properties: merged.properties,
121
+ preferences: merged.preferences,
122
+ utm: merged.utm,
123
+ referrer: merged.referrer,
124
+ omnipotent: merged.omnipotent,
125
+ status: merged.status,
126
+ metadata: merged.metadata,
127
+ // if password is updated require a login (except if confirmation is not done)
128
+ ...data.password ? {
129
+ password: this.hashPassword(data.password),
130
+ status: old.status !== UserStatus.Pending ? UserStatus.Expired : UserStatus.Pending,
131
+ } : {},
132
+ });
133
+
134
+ if (affectedRows <= 0) {
135
+ throw new APIError({
136
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
137
+ detail: `Could not update user with id ${id}`,
138
+ });
139
+ }
140
+
141
+ return await this.get(id, options);
142
+ } catch (e) { throw e; }
143
+
144
+ };
145
+
146
+ private readonly status = async (id: number, status: UserStatus, options: Options): Promise<User[]> => {
147
+ try {
148
+ const old = (await this.get(id, { password: true }))[0];
149
+ if (!old) {
150
+ throw new APIError({
151
+ code: StatusCodes.NOT_FOUND,
152
+ detail: `User with id ${id} not found.`,
153
+ });
154
+ }
155
+
156
+ const { affectedRows } = await userDao.update({ id: id }, {
157
+ ...old,
158
+ status: status,
159
+ });
160
+
161
+ if (affectedRows <= 0) {
162
+ throw new APIError({
163
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
164
+ detail: `Could not update user with id ${id}`,
165
+ });
166
+ }
167
+
168
+ return await this.get(id, options);
169
+ } catch (e) { throw e; }
170
+ };
171
+
172
+ public readonly activate = async (id: number, options: Options): Promise<User[]> =>
173
+ this.status(id, UserStatus.Active, options);
174
+
175
+ public readonly block = async (id: number, options: Options): Promise<User[]> =>
176
+ this.status(id, UserStatus.Blocked, options);
177
+
178
+ public readonly delete = async (id: number): Promise<{ deleted: boolean }> => {
179
+ try {
180
+ const user = (await this.get(id, { password: true }))[0];
181
+ if (!user) {
182
+ throw new APIError({
183
+ code: StatusCodes.NOT_FOUND,
184
+ detail: `User with id ${id} not found.`,
185
+ });
186
+ }
187
+
188
+ // check user organization accesses
189
+ const accesses = await userOrganizationController.list({ user: user.id, enabled: true });
190
+ const owner = accesses.filter((access) => access.role === UserOrganizationRole.Owner);
191
+ if (owner.length > 0) {
192
+ throw new APIError({
193
+ code: StatusCodes.BAD_REQUEST,
194
+ detail: `This user is the owner of ${owner.length} organization${owner.length === 1 ? `` : `s`}. Please remove ${owner.length === 1 ? `it` : `them`} or change the ownership before deleting the user.`,
195
+ });
196
+ }
197
+
198
+ // empty authorizations
199
+ await identityTokenController.deleteAll(user);
200
+
201
+ const { affectedRows } = await userDao.delete({ id: user.id });
202
+
203
+ if (affectedRows <= 0) {
204
+ throw new APIError({
205
+ code: StatusCodes.INTERNAL_SERVER_ERROR,
206
+ detail: `Could not delete user with id ${id}`,
207
+ });
208
+ }
209
+
210
+ return { deleted: affectedRows > 0 };
211
+ } catch (e) { throw e; }
212
+ };
213
+
214
+ public readonly hashPassword = (password: string) =>
215
+ Hash.sha256(password, nconf.get(`SERVICES:AUTH:SALT`));
216
+
217
+ private readonly getFreeUUID = async (): Promise<string> => {
218
+ try {
219
+ let uuid: string;
220
+ do {
221
+ uuid = newUUIDv4({ case: `lower` });
222
+ const exists = (await userDao.list({ uuid: uuid }))[0];
223
+ if (!exists) { break; }
224
+ } while (true);
225
+ return uuid;
226
+ } catch (e) { throw e; }
227
+ };
228
+
229
+ private readonly check = async (data: Add | User, old?: User): Promise<void> => {
230
+ try {
231
+ if (!old || data.email !== old.email) {
232
+ const exists = (await this.list({ email: data.email }, { password: false }))[0];
233
+ if (exists) {
234
+ throw new APIError({
235
+ code: StatusCodes.CONFLICT,
236
+ detail: `User with email ${data.email} already exists.`,
237
+ });
238
+ }
239
+ }
240
+ } catch (e) { throw e; }
241
+ };
242
+
243
+ private readonly parse = async (records: DBRecord[], options: Options): Promise<User[]> => {
244
+ try {
245
+ const response: User[] = [];
246
+
247
+ for (const record of records) {
248
+ if (!_.includes(UserStatus, record.status)) { continue; }
249
+
250
+ const p: User = {
251
+ id: record.id,
252
+ uuid: record.uuid,
253
+ email: record.email,
254
+ password: options.password ? record.password : record.password ? `********` : null,
255
+ properties: {
256
+ name: record.properties.name,
257
+ surnames: record.properties.surnames,
258
+ ...record.properties.image ? { image: `${record.properties.image}${options.timestamp ? `?ts=${performance.now()}` : ``}` } : {},
259
+ },
260
+ preferences: {
261
+ language: record.preferences.language || `en`,
262
+ communications: record.preferences.communications,
263
+ },
264
+ utm: record.utm,
265
+ referrer: record.referrer,
266
+ omnipotent: record.omnipotent ? true : false,
267
+ created_date: new Date2(record.created_date).format(Formats.ISO),
268
+ modified_date: new Date2(record.modified_date).format(Formats.ISO),
269
+ status: record.status,
270
+ metadata: record.metadata,
271
+ };
272
+
273
+ response.push(p);
274
+ }
275
+
276
+ return response;
277
+ } catch (e) { throw e; }
278
+ };
279
+ }
280
+
281
+ const userController = new UserCtrl();
282
+ export default userController;