@internetderdinge/api 1.229.9 → 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.
Files changed (35) hide show
  1. package/dist/src/accounts/accounts.controller.js +0 -1
  2. package/dist/src/config/config.js +0 -6
  3. package/dist/src/devices/devices.controller.js +1 -31
  4. package/dist/src/devices/devices.route.js +5 -33
  5. package/dist/src/devices/devices.service.js +0 -21
  6. package/dist/src/devices/devices.validation.js +3 -40
  7. package/dist/src/email/email.service.js +46 -39
  8. package/dist/src/index.js +19 -1
  9. package/dist/src/iotdevice/iotdevice.route.js +1 -1
  10. package/dist/src/iotdevice/iotdevice.service.js +23 -113
  11. package/dist/src/middlewares/auth.js +49 -2
  12. package/dist/src/middlewares/validateAdmin.js +0 -1
  13. package/dist/src/middlewares/validateCurrentUser.js +2 -2
  14. package/dist/src/models/plugins/simplePopulate.js +1 -1
  15. package/dist/src/pdf/pdf.service.js +18 -19
  16. package/dist/src/users/users.schemas.js +2 -46
  17. package/dist/src/users/users.service.js +1 -0
  18. package/package.json +3 -2
  19. package/src/accounts/accounts.controller.ts +0 -1
  20. package/src/config/config.ts +0 -6
  21. package/src/devices/devices.controller.ts +0 -53
  22. package/src/devices/devices.route.ts +3 -39
  23. package/src/devices/devices.service.ts +0 -38
  24. package/src/devices/devices.validation.ts +9 -47
  25. package/src/email/email.service.ts +70 -43
  26. package/src/index.ts +19 -1
  27. package/src/iotdevice/iotdevice.route.ts +1 -1
  28. package/src/iotdevice/iotdevice.service.ts +34 -167
  29. package/src/middlewares/auth.ts +75 -2
  30. package/src/middlewares/validateAdmin.ts +0 -1
  31. package/src/middlewares/validateCurrentUser.ts +2 -2
  32. package/src/models/plugins/simplePopulate.ts +1 -1
  33. package/src/pdf/pdf.service.ts +36 -31
  34. package/src/users/users.schemas.ts +3 -50
  35. package/src/users/users.service.ts +1 -0
@@ -1,13 +1,20 @@
1
- import AWS from 'aws-sdk';
2
- import type { SES } from 'aws-sdk';
3
- import config from '../../src/config/config';
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('http');
6
+ return url.startsWith("http");
8
7
  }
9
8
 
10
- const button = ({ link, text, color = '#0076ff' }: { link: string; text: string; color?: string }): string => {
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 = ({ link, text, color = '#0076ff' }: { link: string; text: string; color?: string }): string => {
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 = 'Kein Titel',
55
- body = 'Kein Inhalt',
56
- url = '',
57
- domain = 'memo',
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 = '#0076ff';
64
- AWS.config.update({
65
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
66
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
67
- region: 'eu-central-1',
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(actionButtonText || 'Go to Application', { lng });
90
+ const actionButtonTextWithLanguage = i18n.t(
91
+ actionButtonText || "Go to Application",
92
+ { lng },
93
+ );
71
94
 
72
- const ses: SES = new AWS.SES({ apiVersion: '2010-12-01' });
73
-
74
- const toEmail = 'notifications@wirewire.de';
75
- const base64ToName = Buffer.from(`Memo ${i18n.t('Notifications', { lng })}`).toString('base64');
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: SES.SendEmailRequest = {
101
+ const params = {
79
102
  Destination: {
80
103
  ToAddresses: [email],
81
104
  },
82
- ConfigurationSetName: 'memo-transactional',
83
- Message: {
84
- Body: {
85
- Html: {
86
- Charset: 'UTF-8',
87
- Data: `
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 === 'memo' ? 'ANABOX smart' : 'paperlesspaper'}
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) ? url : `http://${domain}.wirewire.de${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 === 'web' ? 'The Wire UG' : 'wirewire GmbH'}
598
+ ${domain === "web" ? "The Wire UG" : "wirewire GmbH"}
573
599
  <a href="http://${domain}.wirewire.de/account">Account</a>
574
600
 
575
- ${config.env !== 'production' ? `<br/><br/>Environment: ${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
- Text: {
591
- Charset: 'UTF-8',
592
- Data: `${title} ${body}`,
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
- Source: finalToName,
627
+ FromEmailAddress: finalToName,
601
628
  };
602
629
 
603
630
  try {
604
- const data = await ses.sendEmail(params).promise();
605
- console.log('Email submitted to SES', data);
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,8 +13,10 @@ 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";
19
+ export { auth0 } from "../src/accounts/auth0.service";
18
20
  export { default as organizationsRoute } from "../src/organizations/organizations.route";
19
21
  export { default as organizationsService } from "../src/organizations/organizations.service";
20
22
  export { default as Organization } from "../src/organizations/organizations.model";
@@ -25,12 +27,12 @@ export * from "../src/devices/devices.validation";
25
27
  export { default as devicesNotificationsRoute } from "./devicesNotifications/devicesNotifications.route";
26
28
  export { default as devicesNotificationsService } from "../src/devicesNotifications/devicesNotifications.service";
27
29
  export { default as iotDevicesService } from "../src/iotdevice/iotdevice.service";
30
+ export { default as iotdeviceRoute } from "../src/iotdevice/iotdevice.route";
28
31
  export { SIMILARITY_THRESHOLD } from "../src/iotdevice/iotdevice.service";
29
32
  export { default as pdfRoute } from "../src/pdf/pdf.route";
30
33
  export { default as tokensRoute } from "../src/tokens/tokens.route";
31
34
  export * from "../src/tokens/tokens.service";
32
35
  export { default as Token } from "../src/tokens/tokens.model";
33
- export * as usersService from "../src/users/users.service";
34
36
  export { User } from "../src/users/users.model";
35
37
  export { isAdmin, validateAdmin } from "../src/middlewares/validateAdmin";
36
38
  export { sendEmail } from "../src/email/email.service";
@@ -42,6 +44,7 @@ export { paginate, toJSON } from "../src/models/plugins/index";
42
44
  export { compareImages } from "../src/utils/comparePapers.service";
43
45
  export { resolvePossiblyRelativeUrl } from "../src/utils/urlUtils";
44
46
  export { getSignedFileUrl } from "../src/files/upload.service";
47
+ export * from "../src/middlewares/rateLimiter";
45
48
  export * from "../src/utils/ApiError";
46
49
  export * from "../src/utils/buildRouterAndDocs";
47
50
  export * from "../src/utils/comparePapers.service";
@@ -53,3 +56,18 @@ export * from "../src/utils/registerOpenApi";
53
56
  export * from "../src/utils/urlUtils";
54
57
  export * from "../src/utils/userName";
55
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 AWS from "aws-sdk";
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 { uploadImage, getSignedFileUrl } from "../files/upload.service";
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
- type UploadSingleImageParams = {
26
- deviceName: string;
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 buildUploadResponse = (
34
- data: any,
35
- similarityPercentage: number | null,
36
- skippedUpload: boolean,
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 iotdata.getThingShadow(params).promise();
337
+ const response = await iotDataClient.send(
338
+ new GetThingShadowCommand({
339
+ thingName: deviceName,
340
+ shadowName,
341
+ }),
342
+ );
407
343
 
408
- return JSON.parse(response.payload);
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 iotdata.updateThingShadow(params).promise();
460
- return JSON.parse(response.payload);
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,
@@ -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
- "https://memo.wirewire.de/roles": roles,
145
+ [ROLES_CLAIM]: auth0Roles,
73
146
  };
74
147
  return next();
75
148
  }
@@ -3,7 +3,6 @@ import ApiError from "../utils/ApiError";
3
3
  import type { Request, Response, NextFunction } from "express";
4
4
 
5
5
  const isAdmin = (user: Record<string, any> | undefined): boolean => {
6
- return false;
7
6
  if (!user) return false;
8
7
 
9
8
  // return false; // TODO: Remove this line when the user object is properly defined
@@ -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 getCurrentUser = async (
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 getCurrentUser;
35
+ export default validateCurrentUser;
@@ -1,4 +1,4 @@
1
- function simplePopulate(populate: string): Record<string, any> {
1
+ export function simplePopulate(populate: string): Record<string, any> {
2
2
  let docsPromise: Record<string, any> = {};
3
3
  populate.split(",").forEach((populateOption) => {
4
4
  docsPromise = populateOption