@internetderdinge/api 1.229.10 → 1.229.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/accounts/accounts.controller.js +0 -1
- package/dist/src/config/config.js +0 -6
- package/dist/src/devices/devices.controller.js +1 -31
- package/dist/src/devices/devices.route.js +2 -33
- package/dist/src/devices/devices.service.js +0 -21
- package/dist/src/devices/devices.validation.js +3 -40
- package/dist/src/email/email.service.js +46 -39
- package/dist/src/index.js +18 -1
- package/dist/src/iotdevice/iotdevice.route.js +1 -1
- package/dist/src/iotdevice/iotdevice.service.js +23 -113
- package/dist/src/middlewares/auth.js +49 -2
- package/dist/src/middlewares/validateCurrentUser.js +2 -2
- package/dist/src/models/plugins/simplePopulate.js +1 -1
- package/dist/src/pdf/pdf.service.js +18 -19
- package/dist/src/users/users.schemas.js +2 -46
- package/dist/src/users/users.service.js +1 -0
- package/package.json +3 -2
- package/src/accounts/accounts.controller.ts +0 -1
- package/src/config/config.ts +0 -6
- package/src/devices/devices.controller.ts +0 -53
- package/src/devices/devices.route.ts +0 -39
- package/src/devices/devices.service.ts +0 -38
- package/src/devices/devices.validation.ts +9 -47
- package/src/email/email.service.ts +70 -43
- package/src/index.ts +18 -1
- package/src/iotdevice/iotdevice.route.ts +1 -1
- package/src/iotdevice/iotdevice.service.ts +34 -167
- package/src/middlewares/auth.ts +75 -2
- package/src/middlewares/validateCurrentUser.ts +2 -2
- package/src/models/plugins/simplePopulate.ts +1 -1
- package/src/pdf/pdf.service.ts +36 -31
- package/src/users/users.schemas.ts +3 -50
- package/src/users/users.service.ts +1 -0
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import i18n from '../../src/i18n/i18n';
|
|
1
|
+
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
2
|
+
import config from "../../src/config/config";
|
|
3
|
+
import i18n from "../../src/i18n/i18n";
|
|
5
4
|
|
|
6
5
|
function urlStartsWithHttp(url: string): boolean {
|
|
7
|
-
return url.startsWith(
|
|
6
|
+
return url.startsWith("http");
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
const button = ({
|
|
9
|
+
const button = ({
|
|
10
|
+
link,
|
|
11
|
+
text,
|
|
12
|
+
color = "#0076ff",
|
|
13
|
+
}: {
|
|
14
|
+
link: string;
|
|
15
|
+
text: string;
|
|
16
|
+
color?: string;
|
|
17
|
+
}): string => {
|
|
11
18
|
return `
|
|
12
19
|
<div><!--[if mso]>
|
|
13
20
|
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${link}" style="height:40px;v-text-anchor:middle;width:200px;" arcsize="15%" stroke="f" fillcolor="${color}">
|
|
@@ -23,7 +30,15 @@ const button = ({ link, text, color = '#0076ff' }: { link: string; text: string;
|
|
|
23
30
|
`;
|
|
24
31
|
};
|
|
25
32
|
|
|
26
|
-
const actionButton = ({
|
|
33
|
+
const actionButton = ({
|
|
34
|
+
link,
|
|
35
|
+
text,
|
|
36
|
+
color = "#0076ff",
|
|
37
|
+
}: {
|
|
38
|
+
link: string;
|
|
39
|
+
text: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
}): string => {
|
|
27
42
|
return `<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
|
28
43
|
<tr>
|
|
29
44
|
<td align="center">
|
|
@@ -51,40 +66,49 @@ interface SendEmailParams {
|
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
export const sendEmail = async ({
|
|
54
|
-
title =
|
|
55
|
-
body =
|
|
56
|
-
url =
|
|
57
|
-
domain =
|
|
69
|
+
title = "Kein Titel",
|
|
70
|
+
body = "Kein Inhalt",
|
|
71
|
+
url = "",
|
|
72
|
+
domain = "memo",
|
|
58
73
|
image,
|
|
59
74
|
email,
|
|
60
75
|
actionButtonText,
|
|
61
76
|
lng,
|
|
62
77
|
}: SendEmailParams): Promise<void> => {
|
|
63
|
-
const interactive =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
const interactive = "#0076ff";
|
|
79
|
+
const sesClient = new SESv2Client({
|
|
80
|
+
region: "eu-central-1",
|
|
81
|
+
credentials:
|
|
82
|
+
process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
|
|
83
|
+
? {
|
|
84
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
85
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
68
88
|
});
|
|
69
89
|
|
|
70
|
-
const actionButtonTextWithLanguage = i18n.t(
|
|
90
|
+
const actionButtonTextWithLanguage = i18n.t(
|
|
91
|
+
actionButtonText || "Go to Application",
|
|
92
|
+
{ lng },
|
|
93
|
+
);
|
|
71
94
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
const toEmail = "notifications@wirewire.de";
|
|
96
|
+
const base64ToName = Buffer.from(
|
|
97
|
+
`Memo ${i18n.t("Notifications", { lng })}`,
|
|
98
|
+
).toString("base64");
|
|
76
99
|
const finalToName = `=?UTF-8?B?${base64ToName}?= <${toEmail}>`;
|
|
77
100
|
|
|
78
|
-
const params
|
|
101
|
+
const params = {
|
|
79
102
|
Destination: {
|
|
80
103
|
ToAddresses: [email],
|
|
81
104
|
},
|
|
82
|
-
ConfigurationSetName:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
ConfigurationSetName: "memo-transactional",
|
|
106
|
+
Content: {
|
|
107
|
+
Simple: {
|
|
108
|
+
Body: {
|
|
109
|
+
Html: {
|
|
110
|
+
Charset: "UTF-8",
|
|
111
|
+
Data: `
|
|
88
112
|
<!DOCTYPE html>
|
|
89
113
|
<html>
|
|
90
114
|
<head>
|
|
@@ -537,7 +561,7 @@ export const sendEmail = async ({
|
|
|
537
561
|
<tr>
|
|
538
562
|
<td class="email-masthead">
|
|
539
563
|
<a href="https://${domain}.wirewire.de" class="f-fallback email-masthead_name">
|
|
540
|
-
${domain ===
|
|
564
|
+
${domain === "memo" ? "ANABOX smart" : "paperlesspaper"}
|
|
541
565
|
</a>
|
|
542
566
|
</td>
|
|
543
567
|
</tr>
|
|
@@ -549,12 +573,14 @@ export const sendEmail = async ({
|
|
|
549
573
|
<tr>
|
|
550
574
|
<td class="content-cell align-center">
|
|
551
575
|
<div class="f-fallback">
|
|
552
|
-
${image ? `<img class="email-image" src="${image}" alt="memo image" />` :
|
|
576
|
+
${image ? `<img class="email-image" src="${image}" alt="memo image" />` : ""}
|
|
553
577
|
<h1>${title}</h1>
|
|
554
578
|
<p>${body}</p>
|
|
555
579
|
<!-- Action -->
|
|
556
580
|
${actionButton({
|
|
557
|
-
link: urlStartsWithHttp(url)
|
|
581
|
+
link: urlStartsWithHttp(url)
|
|
582
|
+
? url
|
|
583
|
+
: `http://${domain}.wirewire.de${url}`,
|
|
558
584
|
text: actionButtonTextWithLanguage,
|
|
559
585
|
})}
|
|
560
586
|
</div>
|
|
@@ -569,10 +595,10 @@ export const sendEmail = async ({
|
|
|
569
595
|
<tr>
|
|
570
596
|
<td class="content-cell" align="center">
|
|
571
597
|
<p class="f-fallback sub align-center">
|
|
572
|
-
${domain ===
|
|
598
|
+
${domain === "web" ? "The Wire UG" : "wirewire GmbH"}
|
|
573
599
|
<a href="http://${domain}.wirewire.de/account">Account</a>
|
|
574
600
|
|
|
575
|
-
${config.env !==
|
|
601
|
+
${config.env !== "production" ? `<br/><br/>Environment: ${config.env}` : ""}
|
|
576
602
|
</p>
|
|
577
603
|
</td>
|
|
578
604
|
</tr>
|
|
@@ -586,23 +612,24 @@ export const sendEmail = async ({
|
|
|
586
612
|
</body>
|
|
587
613
|
</html>
|
|
588
614
|
`,
|
|
615
|
+
},
|
|
616
|
+
Text: {
|
|
617
|
+
Charset: "UTF-8",
|
|
618
|
+
Data: `${title} ${body}`,
|
|
619
|
+
},
|
|
589
620
|
},
|
|
590
|
-
|
|
591
|
-
Charset:
|
|
592
|
-
Data: `${title}
|
|
621
|
+
Subject: {
|
|
622
|
+
Charset: "UTF-8",
|
|
623
|
+
Data: `${title} - Memo App`,
|
|
593
624
|
},
|
|
594
625
|
},
|
|
595
|
-
Subject: {
|
|
596
|
-
Charset: 'UTF-8',
|
|
597
|
-
Data: `${title} - Memo App`,
|
|
598
|
-
},
|
|
599
626
|
},
|
|
600
|
-
|
|
627
|
+
FromEmailAddress: finalToName,
|
|
601
628
|
};
|
|
602
629
|
|
|
603
630
|
try {
|
|
604
|
-
const data = await
|
|
605
|
-
console.log(
|
|
631
|
+
const data = await sesClient.send(new SendEmailCommand(params));
|
|
632
|
+
console.log("Email submitted to SES", data);
|
|
606
633
|
} catch (error) {
|
|
607
634
|
console.error(error);
|
|
608
635
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
export { initI18n } from "../src/i18n/i18n";
|
|
14
14
|
export { default as i18n } from "../src/i18n/i18n";
|
|
15
15
|
export { default as usersRoute } from "../src/users/users.route";
|
|
16
|
+
export { default as usersService } from "../src/users/users.service";
|
|
16
17
|
export { default as accountsRoute } from "../src/accounts/accounts.route";
|
|
17
18
|
export { default as accountsService } from "../src/accounts/accounts.service";
|
|
18
19
|
export { auth0 } from "../src/accounts/auth0.service";
|
|
@@ -26,12 +27,12 @@ export * from "../src/devices/devices.validation";
|
|
|
26
27
|
export { default as devicesNotificationsRoute } from "./devicesNotifications/devicesNotifications.route";
|
|
27
28
|
export { default as devicesNotificationsService } from "../src/devicesNotifications/devicesNotifications.service";
|
|
28
29
|
export { default as iotDevicesService } from "../src/iotdevice/iotdevice.service";
|
|
30
|
+
export { default as iotdeviceRoute } from "../src/iotdevice/iotdevice.route";
|
|
29
31
|
export { SIMILARITY_THRESHOLD } from "../src/iotdevice/iotdevice.service";
|
|
30
32
|
export { default as pdfRoute } from "../src/pdf/pdf.route";
|
|
31
33
|
export { default as tokensRoute } from "../src/tokens/tokens.route";
|
|
32
34
|
export * from "../src/tokens/tokens.service";
|
|
33
35
|
export { default as Token } from "../src/tokens/tokens.model";
|
|
34
|
-
export * as usersService from "../src/users/users.service";
|
|
35
36
|
export { User } from "../src/users/users.model";
|
|
36
37
|
export { isAdmin, validateAdmin } from "../src/middlewares/validateAdmin";
|
|
37
38
|
export { sendEmail } from "../src/email/email.service";
|
|
@@ -43,6 +44,7 @@ export { paginate, toJSON } from "../src/models/plugins/index";
|
|
|
43
44
|
export { compareImages } from "../src/utils/comparePapers.service";
|
|
44
45
|
export { resolvePossiblyRelativeUrl } from "../src/utils/urlUtils";
|
|
45
46
|
export { getSignedFileUrl } from "../src/files/upload.service";
|
|
47
|
+
export * from "../src/middlewares/rateLimiter";
|
|
46
48
|
export * from "../src/utils/ApiError";
|
|
47
49
|
export * from "../src/utils/buildRouterAndDocs";
|
|
48
50
|
export * from "../src/utils/comparePapers.service";
|
|
@@ -54,3 +56,18 @@ export * from "../src/utils/registerOpenApi";
|
|
|
54
56
|
export * from "../src/utils/urlUtils";
|
|
55
57
|
export * from "../src/utils/userName";
|
|
56
58
|
export * from "../src/utils/zValidations";
|
|
59
|
+
export * from "../src/validations/custom.validation";
|
|
60
|
+
export * from "../src/models/plugins/paginate.plugin";
|
|
61
|
+
export * from "../src/models/plugins/simplePopulate";
|
|
62
|
+
export * from "../src/middlewares/validateOrganization";
|
|
63
|
+
export * from "../src/middlewares/validateUser";
|
|
64
|
+
export * from "../src/middlewares/validateCurrentUser";
|
|
65
|
+
export * from "../src/middlewares/validateZod";
|
|
66
|
+
export * from "../src/middlewares/validateAdmin";
|
|
67
|
+
export * from "../src/middlewares/validateAi";
|
|
68
|
+
export * from "../src/middlewares/validateDevice";
|
|
69
|
+
export * from "../src/middlewares/auth";
|
|
70
|
+
export * from "../src/middlewares/error";
|
|
71
|
+
export * from "../src/accounts/auth0.service";
|
|
72
|
+
export * from "../src/middlewares/validateAction";
|
|
73
|
+
export { default as generateUserName } from "../src/utils/userName";
|
|
@@ -148,7 +148,7 @@ export const iotdeviceRouteSpecs: RouteSpec[] = [
|
|
|
148
148
|
handler: getApiStatus,
|
|
149
149
|
summary: "Get API status by kind",
|
|
150
150
|
description:
|
|
151
|
-
"Retrieves the API status information for a given status kind.",
|
|
151
|
+
"Retrieves the IoT API status information for a given status kind to monitor system health or performance. Can be accessed without authentication for monitoring purposes.",
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
method: "get",
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
import httpStatus from "http-status";
|
|
3
3
|
import axios from "axios";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
GetThingShadowCommand,
|
|
6
|
+
IoTDataPlaneClient,
|
|
7
|
+
UpdateThingShadowCommand,
|
|
8
|
+
} from "@aws-sdk/client-iot-data-plane";
|
|
5
9
|
import { deviceKindHasFeature } from "../utils/deviceUtils";
|
|
6
10
|
import ApiError from "../utils/ApiError";
|
|
7
11
|
import { getAuth0Token } from "../accounts/auth0.service";
|
|
8
|
-
import {
|
|
9
|
-
import { compareImages } from "../utils/comparePapers.service";
|
|
12
|
+
import { getSignedFileUrl } from "../files/upload.service";
|
|
10
13
|
import IotDevice from "./iotdevice.model";
|
|
11
|
-
import { fileTypeFromBuffer } from "file-type";
|
|
12
14
|
|
|
13
15
|
import type { AxiosRequestConfig } from "axios";
|
|
14
16
|
import type { Device } from "../devices/devices.model.js";
|
|
@@ -22,76 +24,13 @@ export const SIMILARITY_THRESHOLD = Number(
|
|
|
22
24
|
process.env.EPAPER_SIMILARITY_THRESHOLD ?? 99.995,
|
|
23
25
|
);
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
buffer: Buffer;
|
|
28
|
-
bufferOriginal?: Buffer;
|
|
29
|
-
bufferEditable?: Buffer;
|
|
30
|
-
id: string;
|
|
31
|
-
};
|
|
27
|
+
const IOT_DATA_ENDPOINT =
|
|
28
|
+
"https://a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com";
|
|
32
29
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
) => {
|
|
38
|
-
return { /*...data,*/ key: data?.Key, similarityPercentage, skippedUpload };
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const downloadPreviousOriginalImage = async (
|
|
42
|
-
id: string,
|
|
43
|
-
): Promise<Buffer | null> => {
|
|
44
|
-
if (!id) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const signedUrl = await getSignedFileUrl({
|
|
50
|
-
fileName: `ePaperImages/${id}original.png`,
|
|
51
|
-
});
|
|
52
|
-
const response = await axios.get<ArrayBuffer>(signedUrl, {
|
|
53
|
-
responseType: "arraybuffer",
|
|
54
|
-
});
|
|
55
|
-
return Buffer.from(response.data);
|
|
56
|
-
} catch (error: any) {
|
|
57
|
-
console.warn(
|
|
58
|
-
`Unable to download previous image for ${id}:`,
|
|
59
|
-
error?.message || error,
|
|
60
|
-
);
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const evaluateSimilarityBeforeUpload = async (
|
|
66
|
-
id: string,
|
|
67
|
-
bufferOriginal?: Buffer,
|
|
68
|
-
): Promise<{ similarityPercentage: number | null; skipUpload: boolean }> => {
|
|
69
|
-
if (!bufferOriginal) {
|
|
70
|
-
return { similarityPercentage: null, skipUpload: false };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const previousBuffer = await downloadPreviousOriginalImage(id);
|
|
74
|
-
if (!previousBuffer) {
|
|
75
|
-
return { similarityPercentage: null, skipUpload: false };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const similarityPercentage = await compareImages(
|
|
80
|
-
previousBuffer,
|
|
81
|
-
bufferOriginal,
|
|
82
|
-
);
|
|
83
|
-
return {
|
|
84
|
-
similarityPercentage,
|
|
85
|
-
skipUpload: similarityPercentage >= SIMILARITY_THRESHOLD,
|
|
86
|
-
};
|
|
87
|
-
} catch (error: any) {
|
|
88
|
-
console.warn(
|
|
89
|
-
`Similarity comparison failed for ${id}:`,
|
|
90
|
-
error?.message || error,
|
|
91
|
-
);
|
|
92
|
-
return { similarityPercentage: null, skipUpload: false };
|
|
93
|
-
}
|
|
94
|
-
};
|
|
30
|
+
const iotDataClient = new IoTDataPlaneClient({
|
|
31
|
+
endpoint: IOT_DATA_ENDPOINT,
|
|
32
|
+
region: process.env.AWS_REGION ?? "eu-central-1",
|
|
33
|
+
});
|
|
95
34
|
|
|
96
35
|
/**
|
|
97
36
|
* Get events for a device
|
|
@@ -392,20 +331,20 @@ export const getDeviceStatus = async (deviceName, kind) => {
|
|
|
392
331
|
* @returns {Promise<Device>}
|
|
393
332
|
*/
|
|
394
333
|
export const shadowAlarmGet = async (deviceName, shadowName) => {
|
|
395
|
-
const iotdata = new AWS.IotData({
|
|
396
|
-
endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
|
|
397
|
-
});
|
|
398
|
-
|
|
399
334
|
if (!deviceName) return { error: "deviceName is required" };
|
|
400
|
-
const params = {
|
|
401
|
-
thingName: deviceName,
|
|
402
|
-
shadowName,
|
|
403
|
-
};
|
|
404
335
|
|
|
405
336
|
try {
|
|
406
|
-
const response = await
|
|
337
|
+
const response = await iotDataClient.send(
|
|
338
|
+
new GetThingShadowCommand({
|
|
339
|
+
thingName: deviceName,
|
|
340
|
+
shadowName,
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
407
343
|
|
|
408
|
-
|
|
344
|
+
const payload = response.payload
|
|
345
|
+
? Buffer.from(response.payload).toString("utf8")
|
|
346
|
+
: "{}";
|
|
347
|
+
return JSON.parse(payload);
|
|
409
348
|
} catch (e) {
|
|
410
349
|
// console.log(deviceName, e);
|
|
411
350
|
return "error";
|
|
@@ -442,99 +381,28 @@ export const ledLightHint = async (deviceName, body) => {
|
|
|
442
381
|
const shadowAlarmUpdate = async (deviceName, alarms, shadowName) => {
|
|
443
382
|
const data = alarms;
|
|
444
383
|
|
|
445
|
-
const iotdata = new AWS.IotData({
|
|
446
|
-
endpoint: "a2vm6rc8xrtk10-ats.iot.eu-central-1.amazonaws.com",
|
|
447
|
-
});
|
|
448
|
-
|
|
449
384
|
if (!deviceName) {
|
|
450
385
|
return { error: "no deviceId" };
|
|
451
386
|
}
|
|
452
|
-
const params = {
|
|
453
|
-
payload: JSON.stringify(data),
|
|
454
|
-
thingName: deviceName,
|
|
455
|
-
shadowName,
|
|
456
|
-
};
|
|
457
387
|
|
|
458
388
|
try {
|
|
459
|
-
const response = await
|
|
460
|
-
|
|
389
|
+
const response = await iotDataClient.send(
|
|
390
|
+
new UpdateThingShadowCommand({
|
|
391
|
+
payload: JSON.stringify(data),
|
|
392
|
+
thingName: deviceName,
|
|
393
|
+
shadowName,
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const payload = response.payload
|
|
398
|
+
? Buffer.from(response.payload).toString("utf8")
|
|
399
|
+
: "{}";
|
|
400
|
+
return JSON.parse(payload);
|
|
461
401
|
} catch (e) {
|
|
462
402
|
return "error";
|
|
463
403
|
}
|
|
464
404
|
};
|
|
465
405
|
|
|
466
|
-
export const uploadSingleImage = async ({
|
|
467
|
-
deviceName,
|
|
468
|
-
buffer,
|
|
469
|
-
bufferOriginal,
|
|
470
|
-
bufferEditable,
|
|
471
|
-
id,
|
|
472
|
-
paperId,
|
|
473
|
-
}: UploadSingleImageParams) => {
|
|
474
|
-
try {
|
|
475
|
-
const { skipUpload, similarityPercentage } =
|
|
476
|
-
await evaluateSimilarityBeforeUpload(id, bufferOriginal);
|
|
477
|
-
var response: any = {};
|
|
478
|
-
|
|
479
|
-
if (skipUpload) {
|
|
480
|
-
return buildUploadResponse(
|
|
481
|
-
{ message: "Image skipped due to similarity threshold" },
|
|
482
|
-
similarityPercentage,
|
|
483
|
-
true,
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (!bufferOriginal) {
|
|
488
|
-
throw new Error("bufferOriginal is required to upload an image");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// console.log(`Uploading image for ${id} on ${deviceName}; similarity ${similarityPercentage?.toFixed(5)}%`);
|
|
492
|
-
if (deviceName) {
|
|
493
|
-
const accessToken = await getAuth0Token();
|
|
494
|
-
|
|
495
|
-
response = await axios.post(
|
|
496
|
-
`${process.env.IOT_API_URL_EPAPER}uploads`,
|
|
497
|
-
{ deviceName },
|
|
498
|
-
{
|
|
499
|
-
headers: { Authorization: `Bearer ${accessToken}` },
|
|
500
|
-
},
|
|
501
|
-
);
|
|
502
|
-
if (!response.data.uploadURL) {
|
|
503
|
-
console.log("No upload URL received", response.data);
|
|
504
|
-
} else {
|
|
505
|
-
await axios.put(response.data.uploadURL, buffer, {
|
|
506
|
-
// onUploadProgress: (progressEvent) => console.log('file progress', progressEvent.loaded),
|
|
507
|
-
headers: { "Content-Type": "text/octet-stream" },
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const type = await fileTypeFromBuffer(buffer);
|
|
513
|
-
const fileName = `ePaperImages/${id}`;
|
|
514
|
-
|
|
515
|
-
await uploadImage({ blob: buffer, key: fileName + ".png", type });
|
|
516
|
-
|
|
517
|
-
await uploadImage({
|
|
518
|
-
blob: bufferOriginal,
|
|
519
|
-
key: fileName + "original.png",
|
|
520
|
-
type,
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
if (bufferEditable) {
|
|
524
|
-
await uploadImage({
|
|
525
|
-
blob: bufferEditable,
|
|
526
|
-
key: fileName + "editable.json",
|
|
527
|
-
type,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return buildUploadResponse(response.data, similarityPercentage, false);
|
|
532
|
-
} catch (error) {
|
|
533
|
-
console.error(error);
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
|
|
538
406
|
/**
|
|
539
407
|
* Get device by ID
|
|
540
408
|
* @param {string} id
|
|
@@ -723,7 +591,6 @@ export default {
|
|
|
723
591
|
deleteById,
|
|
724
592
|
getApiStatus,
|
|
725
593
|
getDevice,
|
|
726
|
-
uploadSingleImage,
|
|
727
594
|
liveEventsWs,
|
|
728
595
|
shadowAlarmGet,
|
|
729
596
|
shadowAlarmUpdate,
|
package/src/middlewares/auth.ts
CHANGED
|
@@ -8,6 +8,75 @@ import type { Request, Response, NextFunction } from "express";
|
|
|
8
8
|
import ApiError from "../utils/ApiError";
|
|
9
9
|
import Token from "../tokens/tokens.model";
|
|
10
10
|
import { roleRights } from "../config/roles";
|
|
11
|
+
import auth0Service from "../accounts/auth0.service";
|
|
12
|
+
|
|
13
|
+
const ROLES_CLAIM = "https://memo.wirewire.de/roles";
|
|
14
|
+
const AUTH0_ROLE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
type RoleCacheEntry = {
|
|
17
|
+
roles: string[];
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const auth0RolesCache = new Map<string, RoleCacheEntry>();
|
|
22
|
+
|
|
23
|
+
const dedupeRoles = (roles: string[]): string[] =>
|
|
24
|
+
Array.from(new Set(roles.map((role) => role.trim()).filter(Boolean)));
|
|
25
|
+
|
|
26
|
+
const extractRoleNamesFromManagementResponse = (payload: unknown): string[] => {
|
|
27
|
+
const iterablePayload =
|
|
28
|
+
payload && typeof (payload as any)[Symbol.iterator] === "function"
|
|
29
|
+
? Array.from(payload as Iterable<unknown>)
|
|
30
|
+
: [];
|
|
31
|
+
|
|
32
|
+
const list = Array.isArray(payload)
|
|
33
|
+
? payload
|
|
34
|
+
: Array.isArray((payload as any)?.data)
|
|
35
|
+
? (payload as any).data
|
|
36
|
+
: Array.isArray((payload as any)?.items)
|
|
37
|
+
? (payload as any).items
|
|
38
|
+
: iterablePayload;
|
|
39
|
+
|
|
40
|
+
const roleNames = list
|
|
41
|
+
.map((entry: Record<string, unknown>) =>
|
|
42
|
+
typeof entry?.name === "string" ? entry.name : "",
|
|
43
|
+
)
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
|
|
46
|
+
return dedupeRoles(roleNames);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getAuth0RolesByOwner = async (ownerId: string): Promise<string[]> => {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const cached = auth0RolesCache.get(ownerId);
|
|
52
|
+
if (cached && cached.expiresAt > now) {
|
|
53
|
+
// return cached.roles;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
console.log(
|
|
58
|
+
`Fetching Auth0 roles for owner ${ownerId} from Management API...`,
|
|
59
|
+
);
|
|
60
|
+
const rolesPayload = await (auth0Service as any).auth0.users.roles.list(
|
|
61
|
+
ownerId,
|
|
62
|
+
);
|
|
63
|
+
console.log(`Fetched Auth0 roles for owner ${ownerId}:`, rolesPayload);
|
|
64
|
+
const roles = extractRoleNamesFromManagementResponse(rolesPayload);
|
|
65
|
+
|
|
66
|
+
auth0RolesCache.set(ownerId, {
|
|
67
|
+
roles,
|
|
68
|
+
expiresAt: now + AUTH0_ROLE_CACHE_TTL_MS,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return roles;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn("auth middleware: could not fetch Auth0 roles for owner", {
|
|
74
|
+
ownerId,
|
|
75
|
+
error,
|
|
76
|
+
});
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
};
|
|
11
80
|
|
|
12
81
|
type AuthRequest = Request;
|
|
13
82
|
|
|
@@ -60,8 +129,12 @@ const auth = function authFactory(...requiredRights: string[]) {
|
|
|
60
129
|
|
|
61
130
|
if (tokenDoc) {
|
|
62
131
|
const ownerId = tokenDoc.owner as string;
|
|
63
|
-
const roles = ["api"];
|
|
64
132
|
|
|
133
|
+
const auth0Roles = await getAuth0RolesByOwner(ownerId);
|
|
134
|
+
console.log(
|
|
135
|
+
`Authenticated API token request for owner ${ownerId}`,
|
|
136
|
+
auth0Roles,
|
|
137
|
+
);
|
|
65
138
|
req.auth = {
|
|
66
139
|
id: ownerId,
|
|
67
140
|
tokenId: tokenDoc._id,
|
|
@@ -69,7 +142,7 @@ const auth = function authFactory(...requiredRights: string[]) {
|
|
|
69
142
|
// For API-key auth, we can treat the token owner as the subject.
|
|
70
143
|
// Avoid fetching user profile from Auth0 Management API on every request.
|
|
71
144
|
sub: ownerId,
|
|
72
|
-
|
|
145
|
+
[ROLES_CLAIM]: auth0Roles,
|
|
73
146
|
};
|
|
74
147
|
return next();
|
|
75
148
|
}
|
|
@@ -4,7 +4,7 @@ import userService from "../users/users.service";
|
|
|
4
4
|
|
|
5
5
|
import type { Request, Response, NextFunction } from "express";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
export const validateCurrentUser = async (
|
|
8
8
|
req: Request,
|
|
9
9
|
res: Response,
|
|
10
10
|
next: NextFunction,
|
|
@@ -32,4 +32,4 @@ const getCurrentUser = async (
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export default
|
|
35
|
+
export default validateCurrentUser;
|