@internetderdinge/api 1.224.2

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 (102) hide show
  1. package/.github/copilot-instructions.md +77 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +52 -0
  4. package/package.json +112 -0
  5. package/src/accounts/accounts.controller.ts +166 -0
  6. package/src/accounts/accounts.route.ts +107 -0
  7. package/src/accounts/accounts.schemas.ts +16 -0
  8. package/src/accounts/accounts.service.ts +85 -0
  9. package/src/accounts/accounts.validation.ts +118 -0
  10. package/src/accounts/auth0.service.ts +226 -0
  11. package/src/config/config.ts +49 -0
  12. package/src/config/logger.ts +33 -0
  13. package/src/config/morgan.ts +22 -0
  14. package/src/config/passport.cjs +30 -0
  15. package/src/config/roles.ts +13 -0
  16. package/src/config/tokens.cjs +10 -0
  17. package/src/devices/devices.controller.ts +276 -0
  18. package/src/devices/devices.model.ts +126 -0
  19. package/src/devices/devices.route.ts +198 -0
  20. package/src/devices/devices.schemas.ts +94 -0
  21. package/src/devices/devices.service.ts +320 -0
  22. package/src/devices/devices.validation.ts +221 -0
  23. package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
  24. package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
  25. package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
  26. package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
  27. package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
  28. package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
  29. package/src/email/email.service.ts +609 -0
  30. package/src/files/upload.service.ts +145 -0
  31. package/src/i18n/i18n.ts +51 -0
  32. package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
  33. package/src/index.ts +7 -0
  34. package/src/iotdevice/iotdevice.controller.ts +136 -0
  35. package/src/iotdevice/iotdevice.model.ts +32 -0
  36. package/src/iotdevice/iotdevice.route.ts +181 -0
  37. package/src/iotdevice/iotdevice.schemas.ts +79 -0
  38. package/src/iotdevice/iotdevice.service.ts +732 -0
  39. package/src/iotdevice/iotdevice.validation.ts +61 -0
  40. package/src/middlewares/auth.ts +110 -0
  41. package/src/middlewares/checkJwt.cjs +19 -0
  42. package/src/middlewares/error.js.legacy +44 -0
  43. package/src/middlewares/error.ts +41 -0
  44. package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
  45. package/src/middlewares/rateLimiter.ts +10 -0
  46. package/src/middlewares/validate.ts +25 -0
  47. package/src/middlewares/validateAction.ts +41 -0
  48. package/src/middlewares/validateAdmin.ts +21 -0
  49. package/src/middlewares/validateAi.ts +24 -0
  50. package/src/middlewares/validateCurrentAuthUser.ts +23 -0
  51. package/src/middlewares/validateCurrentUser.ts +35 -0
  52. package/src/middlewares/validateDevice.ts +191 -0
  53. package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
  54. package/src/middlewares/validateOrganization.ts +109 -0
  55. package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
  56. package/src/middlewares/validateTokens.ts +36 -0
  57. package/src/middlewares/validateUser.ts +75 -0
  58. package/src/middlewares/validateZod.ts +54 -0
  59. package/src/models/plugins/index.ts +7 -0
  60. package/src/models/plugins/paginate.plugin.ts +145 -0
  61. package/src/models/plugins/paginateNew.plugin.ts +206 -0
  62. package/src/models/plugins/simplePopulate.ts +12 -0
  63. package/src/models/plugins/toJSON.plugin.ts +51 -0
  64. package/src/organizations/organizations.controller.ts +101 -0
  65. package/src/organizations/organizations.model.ts +62 -0
  66. package/src/organizations/organizations.route.ts +119 -0
  67. package/src/organizations/organizations.schemas.ts +8 -0
  68. package/src/organizations/organizations.service.ts +85 -0
  69. package/src/organizations/organizations.validation.ts +76 -0
  70. package/src/pdf/pdf.controller.ts +18 -0
  71. package/src/pdf/pdf.route.ts +28 -0
  72. package/src/pdf/pdf.schemas.ts +7 -0
  73. package/src/pdf/pdf.service.ts +89 -0
  74. package/src/pdf/pdf.validation.ts +30 -0
  75. package/src/tokens/tokens.controller.ts +81 -0
  76. package/src/tokens/tokens.model.ts +24 -0
  77. package/src/tokens/tokens.route.ts +66 -0
  78. package/src/tokens/tokens.schemas.ts +15 -0
  79. package/src/tokens/tokens.service.ts +46 -0
  80. package/src/tokens/tokens.validation.ts +13 -0
  81. package/src/types/routeSpec.ts +1 -0
  82. package/src/users/users.controller.ts +234 -0
  83. package/src/users/users.model.ts +89 -0
  84. package/src/users/users.route.ts +171 -0
  85. package/src/users/users.schemas.ts +79 -0
  86. package/src/users/users.service.ts +393 -0
  87. package/src/users/users.validation.ts +166 -0
  88. package/src/utils/ApiError.ts +18 -0
  89. package/src/utils/buildRouterAndDocs.ts +85 -0
  90. package/src/utils/catchAsync.ts +9 -0
  91. package/src/utils/comparePapers.service.ts +48 -0
  92. package/src/utils/filterOptions.ts +37 -0
  93. package/src/utils/medicationName.ts +12 -0
  94. package/src/utils/pick.ts +16 -0
  95. package/src/utils/registerOpenApi.ts +32 -0
  96. package/src/utils/urlUtils.ts +14 -0
  97. package/src/utils/userName.ts +27 -0
  98. package/src/utils/zValidations.ts +89 -0
  99. package/src/validations/auth.validation.cjs +60 -0
  100. package/src/validations/custom.validation.ts +26 -0
  101. package/src/validations/index.cjs +2 -0
  102. package/tsconfig.json +22 -0
@@ -0,0 +1,732 @@
1
+ import httpStatus from "http-status";
2
+ import axios from "axios";
3
+ import AWS from "aws-sdk";
4
+ import { deviceKindHasFeature } from "@wirewire/helpers";
5
+ import ApiError from "../utils/ApiError";
6
+ import { getAuth0Token } from "../accounts/auth0.service";
7
+ import {
8
+ uploadFile,
9
+ uploadImage,
10
+ getSignedFileUrl,
11
+ } from "../files/upload.service";
12
+ import { compareImages } from "../utils/comparePapers.service";
13
+ import IotDevice from "./iotdevice.model";
14
+ import fileType from "file-type";
15
+
16
+ import type { AxiosRequestConfig } from "axios";
17
+ import type { Device } from "../devices.model";
18
+ import type { AxiosResponse } from "axios";
19
+
20
+ import iotsdk from "aws-iot-device-sdk-v2";
21
+ const iot = iotsdk.iot;
22
+ const mqtt = iotsdk.mqtt;
23
+
24
+ export const SIMILARITY_THRESHOLD = Number(
25
+ process.env.EPAPER_SIMILARITY_THRESHOLD ?? 99.995,
26
+ );
27
+
28
+ type UploadSingleImageParams = {
29
+ deviceName: string;
30
+ buffer: Buffer;
31
+ bufferOriginal?: Buffer;
32
+ bufferEditable?: Buffer;
33
+ id: string;
34
+ };
35
+
36
+ const buildUploadResponse = (
37
+ data: any,
38
+ similarityPercentage: number | null,
39
+ skippedUpload: boolean,
40
+ ) => {
41
+ return { /*...data,*/ key: data?.Key, similarityPercentage, skippedUpload };
42
+ };
43
+
44
+ const downloadPreviousOriginalImage = async (
45
+ id: string,
46
+ ): Promise<Buffer | null> => {
47
+ if (!id) {
48
+ return null;
49
+ }
50
+
51
+ try {
52
+ const signedUrl = await getSignedFileUrl({
53
+ fileName: `ePaperImages/${id}original.png`,
54
+ });
55
+ const response = await axios.get<ArrayBuffer>(signedUrl, {
56
+ responseType: "arraybuffer",
57
+ });
58
+ return Buffer.from(response.data);
59
+ } catch (error: any) {
60
+ console.warn(
61
+ `Unable to download previous image for ${id}:`,
62
+ error?.message || error,
63
+ );
64
+ return null;
65
+ }
66
+ };
67
+
68
+ const evaluateSimilarityBeforeUpload = async (
69
+ id: string,
70
+ bufferOriginal?: Buffer,
71
+ ): Promise<{ similarityPercentage: number | null; skipUpload: boolean }> => {
72
+ if (!bufferOriginal) {
73
+ return { similarityPercentage: null, skipUpload: false };
74
+ }
75
+
76
+ const previousBuffer = await downloadPreviousOriginalImage(id);
77
+ if (!previousBuffer) {
78
+ return { similarityPercentage: null, skipUpload: false };
79
+ }
80
+
81
+ try {
82
+ const similarityPercentage = await compareImages(
83
+ previousBuffer,
84
+ bufferOriginal,
85
+ );
86
+ return {
87
+ similarityPercentage,
88
+ skipUpload: similarityPercentage >= SIMILARITY_THRESHOLD,
89
+ };
90
+ } catch (error: any) {
91
+ console.warn(
92
+ `Similarity comparison failed for ${id}:`,
93
+ error?.message || error,
94
+ );
95
+ return { similarityPercentage: null, skipUpload: false };
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Get events for a device
101
+ * @param {Object} params
102
+ * @returns {Promise<any>}
103
+ */
104
+ export const getEvents = async (params: {
105
+ DeviceId: string;
106
+ DateStart: string;
107
+ DateEnd: string;
108
+ TypeFilter?: string;
109
+ createdAt?: string;
110
+ }): Promise<any> => {
111
+ const accessToken = await getAuth0Token();
112
+ try {
113
+ const response: AxiosResponse = await axios.get(
114
+ `${process.env.IOT_API_URL}getevent`,
115
+ {
116
+ headers: { Authorization: `Bearer ${accessToken}` },
117
+ params: {
118
+ DeviceId: params.DeviceId,
119
+ DateStart: !params.createdAt
120
+ ? params.DateStart
121
+ : params.DateStart < params.createdAt
122
+ ? params.DateStart
123
+ : params.createdAt,
124
+ DateEnd: params.DateEnd,
125
+ TypeFilter: params.TypeFilter,
126
+ },
127
+ },
128
+ );
129
+ return response.data;
130
+ } catch (error) {
131
+ return null;
132
+ }
133
+ };
134
+
135
+ /**
136
+ * Activate a device
137
+ * @param {string} deviceId
138
+ * @param {string} organization
139
+ * @param {boolean} enable
140
+ * @param {boolean} resetDevice
141
+ * @returns {Promise<any>}
142
+ */
143
+ export const activateDevice = async (
144
+ deviceId: string,
145
+ organization: string,
146
+ enable = true,
147
+ resetDevice = false,
148
+ ): Promise<any> => {
149
+ const accessToken = await getAuth0Token();
150
+
151
+ try {
152
+ const data = {
153
+ deviceName: deviceId,
154
+ organizationName: organization,
155
+ reset: resetDevice,
156
+ enable,
157
+ };
158
+
159
+ const config: AxiosRequestConfig = {
160
+ method: "post",
161
+ url: `${deviceId.slice(0, 3) === "epd" ? process.env.IOT_API_URL_EPAPER : process.env.IOT_API_URL}activatedevice`,
162
+ headers: {
163
+ Authorization: `Bearer ${accessToken}`,
164
+ },
165
+ data,
166
+ };
167
+
168
+ const response: AxiosResponse = await axios(config);
169
+ return response.data;
170
+ } catch (error) {
171
+ console.error(error);
172
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get activated");
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Ping device
178
+ * @param {string} deviceId
179
+ * @returns {Promise<Device>}
180
+ */
181
+ export const pingDevice = async (deviceId, body) => {
182
+ const accessToken = await getAuth0Token();
183
+ try {
184
+ const config = {
185
+ method: "get",
186
+ url: `${process.env.IOT_API_URL}ping`,
187
+ params: {
188
+ DeviceId: deviceId,
189
+ ...body,
190
+ },
191
+ headers: {
192
+ Authorization: `Bearer ${accessToken}`,
193
+ },
194
+ };
195
+
196
+ const response = await axios(config);
197
+ return response.data;
198
+ } catch (error) {
199
+ //console.log(error);
200
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get pinged");
201
+ }
202
+ };
203
+
204
+ /**
205
+ * Reboot device
206
+ * @param {String} deviveId
207
+ * @returns {Promise<Device>}
208
+ */
209
+ export const rebootDevice = async (deviceId) => {
210
+ const accessToken = await getAuth0Token();
211
+ try {
212
+ const config = {
213
+ method: "get",
214
+ url: `${process.env.IOT_API_URL}rebootdevice?DeviceId=${deviceId}`,
215
+ headers: {
216
+ Authorization: `Bearer ${accessToken}`,
217
+ },
218
+ };
219
+
220
+ const response = await axios(config);
221
+ return response.data;
222
+ } catch (error) {
223
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t reboot");
224
+ }
225
+ };
226
+
227
+ /**
228
+ * Update device meta
229
+ * @param {String} deviveId
230
+ * @returns {Promise<Device>}
231
+ */
232
+ export const updateDevice = async (deviceId, meta) => {
233
+ const accessToken = await getAuth0Token();
234
+ try {
235
+ const config = {
236
+ method: "post",
237
+ url: `${process.env.IOT_API_URL}updatedevice`,
238
+ headers: {
239
+ Authorization: `Bearer ${accessToken}`,
240
+ },
241
+ data: {
242
+ deviceName: deviceId,
243
+ ...meta,
244
+ },
245
+ };
246
+
247
+ const response = await axios(config);
248
+ return response.data;
249
+ } catch (error) {
250
+ console.log(error);
251
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t update meta");
252
+ }
253
+ };
254
+
255
+ /**
256
+ * Case status of device
257
+ * @param {String} deviveId
258
+ * @returns {Promise<Device>}
259
+ */
260
+ const caseStatus = async (deviceId) => {
261
+ const accessToken = await getAuth0Token();
262
+ try {
263
+ const config = {
264
+ method: "get",
265
+ url: `${process.env.IOT_API_URL}getcase?DeviceId=${deviceId}`,
266
+ headers: {
267
+ Authorization: `Bearer ${accessToken}`,
268
+ },
269
+ };
270
+
271
+ const response = await axios(config);
272
+ return response.data;
273
+ } catch (error) {
274
+ //console.log(error);
275
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get status");
276
+ }
277
+ };
278
+
279
+ /**
280
+ * Case status of device
281
+ * @param {String} deviveId
282
+ * @returns {Promise<Device>}
283
+ */
284
+ export const alarmsDevice = async (deviceId) => {
285
+ const accessToken = await getAuth0Token();
286
+ try {
287
+ const config = {
288
+ method: "get",
289
+ url: `${process.env.IOT_API_URL}getcase?DeviceId=${deviceId}`,
290
+ headers: {
291
+ Authorization: `Bearer ${accessToken}`,
292
+ },
293
+ };
294
+
295
+ const response = await axios(config);
296
+ return response.data;
297
+ } catch (error) {
298
+ // console.log(error);
299
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get status");
300
+ }
301
+ };
302
+
303
+ /**
304
+ * Get device information
305
+ * @param {Object} userBody
306
+ * @returns {Promise<Device>}
307
+ */
308
+ export const getDevice = async (deviceName) => {
309
+ const accessToken = await getAuth0Token();
310
+ try {
311
+ const response = await axios.post(
312
+ `${process.env.IOT_API_URL}getdevicelist`,
313
+ { deviceName },
314
+ {
315
+ headers: { Authorization: `Bearer ${accessToken}` },
316
+ },
317
+ );
318
+
319
+ return response.data;
320
+ } catch (error) {
321
+ console.error(error.message);
322
+ return null;
323
+ }
324
+ };
325
+
326
+ /**
327
+ * Get device information
328
+ * shows battery level, signal strength, and other information
329
+ * @param {Object} userBody
330
+ * @returns {Promise<Device>}
331
+ */
332
+ export const getDeviceStatusList = async (deviceName) => {
333
+ const accessToken = await getAuth0Token();
334
+
335
+ try {
336
+ const response = await axios.post(
337
+ `${process.env.IOT_API_URL}getdevicestatuslist`,
338
+ {
339
+ returnAll: true,
340
+ DeviceId: [],
341
+ },
342
+ {
343
+ headers: { Authorization: `Bearer ${accessToken}` },
344
+ },
345
+ );
346
+ // console.log('response', response.data);
347
+ return response.data.message;
348
+ } catch (error) {
349
+ console.error(error);
350
+ console.error(`getDeviceStatusList device error: ${deviceName}`);
351
+ return null;
352
+ }
353
+ };
354
+
355
+ /**
356
+ * Get device information
357
+ * @param {Object} userBody
358
+ * @returns {Promise<Device>}
359
+ */
360
+ export const getDeviceStatus = async (deviceName, kind) => {
361
+ const accessToken = await getAuth0Token();
362
+
363
+ try {
364
+ const response = await axios.get(
365
+ `${deviceKindHasFeature("epaper", kind) ? process.env.IOT_API_URL_EPAPER : process.env.IOT_API_URL}getdevicestatus`,
366
+ {
367
+ params: { DeviceId: deviceName },
368
+ headers: { Authorization: `Bearer ${accessToken}` },
369
+ },
370
+ );
371
+
372
+ // console.log('getDeviceStatus response', response.data);
373
+ // TODO: Align return values
374
+ return deviceKindHasFeature("epaper", kind)
375
+ ? response.data
376
+ : response.data.message;
377
+ } catch (error) {
378
+ console.error(
379
+ `getDeviceStatus device error: ${deviceName}`,
380
+ error.message,
381
+ error.statusMessage,
382
+ );
383
+ return null;
384
+ }
385
+ };
386
+
387
+ /**
388
+ * Shadow alarm update
389
+ * @param {Object} userBody
390
+ * @returns {Promise<Device>}
391
+ */
392
+ export const shadowAlarmGet = async (deviceName, shadowName) => {
393
+ const iotdata = new AWS.IotData({
394
+ endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
395
+ });
396
+
397
+ if (!deviceName) return { error: "deviceName is required" };
398
+ const params = {
399
+ thingName: deviceName,
400
+ shadowName,
401
+ };
402
+
403
+ try {
404
+ const response = await iotdata.getThingShadow(params).promise();
405
+
406
+ return JSON.parse(response.payload);
407
+ } catch (e) {
408
+ // console.log(deviceName, e);
409
+ return "error";
410
+ }
411
+ };
412
+
413
+ /**
414
+ * LED update with Timeout
415
+ * @param {Object} userBody
416
+ * @returns {Promise<Device>}
417
+ */
418
+ export const ledLightHint = async (deviceName, body) => {
419
+ const accessToken = await getAuth0Token();
420
+ try {
421
+ const response = await axios.post(
422
+ `${process.env.IOT_API_URL}setled?DeviceId=${deviceName}`,
423
+ body,
424
+ {
425
+ headers: { Authorization: `Bearer ${accessToken}` },
426
+ },
427
+ );
428
+ return response.data;
429
+ } catch (error) {
430
+ console.error(error);
431
+ return null;
432
+ }
433
+ };
434
+
435
+ /**
436
+ * Shadow alarm update
437
+ * @param {Object} userBody
438
+ * @returns {Promise<Device>}
439
+ */
440
+ const shadowAlarmUpdate = async (deviceName, alarms, shadowName) => {
441
+ const data = alarms;
442
+
443
+ const iotdata = new AWS.IotData({
444
+ endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
445
+ });
446
+
447
+ if (!deviceName) {
448
+ return { error: "no deviceId" };
449
+ }
450
+ const params = {
451
+ payload: JSON.stringify(data),
452
+ thingName: deviceName,
453
+ shadowName,
454
+ };
455
+
456
+ try {
457
+ const response = await iotdata.updateThingShadow(params).promise();
458
+ return JSON.parse(response.payload);
459
+ } catch (e) {
460
+ return "error";
461
+ }
462
+ };
463
+
464
+ export const uploadSingleImage = async ({
465
+ deviceName,
466
+ buffer,
467
+ bufferOriginal,
468
+ bufferEditable,
469
+ id,
470
+ paperId,
471
+ }: UploadSingleImageParams) => {
472
+ try {
473
+ const { skipUpload, similarityPercentage } =
474
+ await evaluateSimilarityBeforeUpload(id, bufferOriginal);
475
+ var response: any = {};
476
+
477
+ if (skipUpload) {
478
+ return buildUploadResponse(
479
+ { message: "Image skipped due to similarity threshold" },
480
+ similarityPercentage,
481
+ true,
482
+ );
483
+ }
484
+
485
+ if (!bufferOriginal) {
486
+ throw new Error("bufferOriginal is required to upload an image");
487
+ }
488
+
489
+ // console.log(`Uploading image for ${id} on ${deviceName}; similarity ${similarityPercentage?.toFixed(5)}%`);
490
+ if (deviceName) {
491
+ const accessToken = await getAuth0Token();
492
+
493
+ response = await axios.post(
494
+ `${process.env.IOT_API_URL_EPAPER}uploads`,
495
+ { deviceName },
496
+ {
497
+ headers: { Authorization: `Bearer ${accessToken}` },
498
+ },
499
+ );
500
+ if (!response.data.uploadURL) {
501
+ console.log("No upload URL received", response.data);
502
+ } else {
503
+ await axios.put(response.data.uploadURL, buffer, {
504
+ // onUploadProgress: (progressEvent) => console.log('file progress', progressEvent.loaded),
505
+ headers: { "Content-Type": "text/octet-stream" },
506
+ });
507
+ }
508
+ }
509
+
510
+ const type = await fileType(buffer);
511
+ const fileName = `ePaperImages/${id}`;
512
+
513
+ await uploadImage({ blob: buffer, key: fileName + ".png", type });
514
+
515
+ await uploadImage({
516
+ blob: bufferOriginal,
517
+ key: fileName + "original.png",
518
+ type,
519
+ });
520
+
521
+ if (bufferEditable) {
522
+ await uploadImage({
523
+ blob: bufferEditable,
524
+ key: fileName + "editable.json",
525
+ type,
526
+ });
527
+ }
528
+
529
+ return buildUploadResponse(response.data, similarityPercentage, false);
530
+ } catch (error) {
531
+ console.error(error);
532
+ return null;
533
+ }
534
+ };
535
+
536
+ /**
537
+ * Get device by ID
538
+ * @param {string} id
539
+ * @returns {Promise<Device | null>}
540
+ */
541
+ export const getById = async (id: string): Promise<Device | null> => {
542
+ return IotDevice.findById(id);
543
+ };
544
+
545
+ /**
546
+ * Update device by ID
547
+ * @param {string} userId
548
+ * @param {Partial<Device>} updateBody
549
+ * @returns {Promise<Device>}
550
+ */
551
+ export const updateById = async (
552
+ userId: string,
553
+ updateBody: Partial<Device>,
554
+ ): Promise<Device> => {
555
+ const iotDevice = await getById(userId);
556
+ if (!iotDevice) {
557
+ throw new ApiError(httpStatus.NOT_FOUND, "IoT Device not found");
558
+ }
559
+ Object.assign(iotDevice, updateBody);
560
+ await iotDevice.save();
561
+ return iotDevice;
562
+ };
563
+
564
+ /**
565
+ * Delete device by ID
566
+ * @param {string} userId
567
+ * @returns {Promise<Device>}
568
+ */
569
+ export const deleteById = async (userId: string): Promise<Device> => {
570
+ const iotDevice = await getById(userId);
571
+ if (!iotDevice) {
572
+ throw new ApiError(httpStatus.NOT_FOUND, "IoT Device not found");
573
+ }
574
+ await iotDevice.deleteOne();
575
+ return iotDevice;
576
+ };
577
+
578
+ export const getApiStatus = async (kind: string): Promise<any> => {
579
+ const accessToken = await getAuth0Token();
580
+ try {
581
+ const response: AxiosResponse = await axios.get(
582
+ `${process.env.IOT_API_URL}status/${kind}`,
583
+ {
584
+ headers: { Authorization: `Bearer ${accessToken}` },
585
+ },
586
+ );
587
+ return response.data;
588
+ } catch (error) {
589
+ return null;
590
+ }
591
+ };
592
+
593
+ export const resetDevice = async (deviceId: string): Promise<any> => {
594
+ const accessToken = await getAuth0Token();
595
+
596
+ try {
597
+ const response: AxiosResponse = await axios.get(
598
+ `${process.env.IOT_API_URL}resetdevice`,
599
+ {
600
+ params: {
601
+ DeviceId: deviceId,
602
+ },
603
+ headers: { Authorization: `Bearer ${accessToken}` },
604
+ },
605
+ );
606
+ return response.data;
607
+ } catch (error) {
608
+ //console.error(error);
609
+ throw new ApiError(httpStatus.NOT_FOUND, "Device couldn`t get reset");
610
+ }
611
+ };
612
+
613
+ export async function subscribeToLiveEvents(deviceName: string) {
614
+ const clientId = `web-client-${deviceName}-${Date.now()}`;
615
+
616
+ const client = new mqtt.MqttClient();
617
+ const config =
618
+ iot.AwsIotMqttConnectionConfigBuilder.new_builder_for_websocket()
619
+ .with_clean_session(true)
620
+ .with_client_id(clientId)
621
+ .with_endpoint("a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com")
622
+ .with_credentials(
623
+ process.env.AWS_REGION,
624
+ process.env.AWS_ACCESS_KEY_ID,
625
+ process.env.AWS_SECRET_ACCESS_KEY,
626
+ // currentCredentials.sessionToken,
627
+ )
628
+ .with_keep_alive_seconds(60)
629
+ .build();
630
+
631
+ const mqttClient = client.new_connection(config);
632
+
633
+ console.log(`Subscribed to live events for device: ${deviceName}`);
634
+
635
+ try {
636
+ // 1) actually start the connection
637
+ await mqttClient.connect();
638
+ console.log(`✓ MQTT connected for device: ${deviceName}`);
639
+
640
+ // 2) subscribe to the correct topic
641
+
642
+ const topic = `${deviceName}/send`;
643
+ // subscribe to topic; rely on global 'message' handler to forward payloads
644
+ await mqttClient.subscribe(topic, mqtt.QoS.AtLeastOnce);
645
+ console.log(
646
+ `✓ Subscribed to live events for device: ${deviceName} on topic: ${topic}`,
647
+ );
648
+ } catch (err: any) {
649
+ console.error("⚠️ MQTT Unexpected error:", err.message || err);
650
+ throw err;
651
+ }
652
+
653
+ mqttClient.on("connect", () => {
654
+ mqttClient.subscribe(`nrf-351358815278525/send`);
655
+ });
656
+
657
+ mqttClient.on("message", (topic, payload) => {
658
+ const msg = payload.toString();
659
+ console.log(`🛰 [${topic}]`, msg);
660
+ // emit to your socket server, save to db, whatever…
661
+ });
662
+
663
+ mqttClient.on("error", (err) => {
664
+ console.error("MQTT Error", err);
665
+ });
666
+
667
+ return mqttClient;
668
+ }
669
+
670
+ // NEW:
671
+ export const liveEventsWs = async (
672
+ ws: WebSocket,
673
+ req: Request & { params: { deviceId: string } },
674
+ ) => {
675
+ console.log("WebSocket connection established for live events");
676
+ // lookup the real Thing name
677
+ /* const device = await devicesService.getById(req.params.deviceId);
678
+ if (!device) {
679
+ ws.send(JSON.stringify({ error: 'device not found' }));
680
+ return ws.close();
681
+ }*/
682
+
683
+ // subscribe via MQTT
684
+ const client = await subscribeToLiveEvents(req.params.deviceId);
685
+
686
+ client.on("message", (_topic, payload) => {
687
+ // normalize various binary payload types to string
688
+ let msg: string;
689
+ if (payload instanceof ArrayBuffer) {
690
+ msg = Buffer.from(payload).toString("utf8");
691
+ } else if (ArrayBuffer.isView(payload)) {
692
+ // Uint8Array, DataView, etc.
693
+ msg = Buffer.from(payload as Uint8Array).toString("utf8");
694
+ } else {
695
+ msg = payload.toString();
696
+ }
697
+ console.log("🛰 Forwarding MQTT → WS:", msg);
698
+ ws.send(msg);
699
+ });
700
+
701
+ // wait 4 1 second before sending a message to the client
702
+ console.log("Waiting 1 second before sending initial message to client...");
703
+ await new Promise((resolve) => setTimeout(resolve, 3000));
704
+ // send initial connection established message and then every 10 seconds
705
+ ws.send(JSON.stringify({ message: "WebSocket connection established" }));
706
+ const intervalId = setInterval(() => {
707
+ ws.send(JSON.stringify({ message: "send message" }));
708
+ }, 10000);
709
+
710
+ ws.on("close", () => {
711
+ clearInterval(intervalId);
712
+ client.disconnect();
713
+ });
714
+ };
715
+
716
+ export default {
717
+ activateDevice,
718
+ getEvents,
719
+ getById,
720
+ updateById,
721
+ deleteById,
722
+ getApiStatus,
723
+ getDevice,
724
+ uploadSingleImage,
725
+ liveEventsWs,
726
+ shadowAlarmGet,
727
+ shadowAlarmUpdate,
728
+ ledLightHint,
729
+ getDeviceStatus,
730
+ getDeviceStatusList,
731
+ subscribeToLiveEvents,
732
+ };