@tmlmobilidade/interfaces 20251007.1345.37-staging.0 → 20251008.1352.27-staging.0
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/enrich-user-refs.d.ts +4 -0
- package/dist/src/enrich-user-refs.js +75 -0
- package/dist/src/interfaces/auth/users.d.ts +7 -8
- package/dist/src/interfaces/auth/users.js +1 -0
- package/dist/src/interfaces/notifications/notifications.d.ts +9 -0
- package/dist/src/interfaces/notifications/notifications.js +56 -18
- package/dist/src/mongo-collection.d.ts +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { users } from './interfaces/auth/users.js';
|
|
3
|
+
import { UserDisplayFields } from '@tmlmobilidade/types';
|
|
4
|
+
const isUserRefKey = (k) => k === 'created_by' || k === 'updated_by';
|
|
5
|
+
const isEligibleId = (v) => typeof v === 'string' && v && v !== 'system';
|
|
6
|
+
function collectUserIds(input, out) {
|
|
7
|
+
if (Array.isArray(input)) {
|
|
8
|
+
for (const item of input)
|
|
9
|
+
collectUserIds(item, out);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (input && typeof input === 'object') {
|
|
13
|
+
for (const [k, v] of Object.entries(input)) {
|
|
14
|
+
if (isUserRefKey(k) && isEligibleId(v))
|
|
15
|
+
out.add(v);
|
|
16
|
+
collectUserIds(v, out);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function replaceRefs(input, map) {
|
|
21
|
+
if (Array.isArray(input)) {
|
|
22
|
+
return input.map(item => replaceRefs(item, map));
|
|
23
|
+
}
|
|
24
|
+
if (input && typeof input === 'object') {
|
|
25
|
+
const obj = input;
|
|
26
|
+
const out = Array.isArray(obj) ? [] : {};
|
|
27
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
28
|
+
if (isUserRefKey(k) && typeof v === 'string' && v !== 'system') {
|
|
29
|
+
const found = map.get(v);
|
|
30
|
+
out[k] = found ?? v; // fallback to original id if user not found
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
out[k] = replaceRefs(v, map);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
return input;
|
|
39
|
+
}
|
|
40
|
+
async function fetchUsersMap(ids) {
|
|
41
|
+
if (ids.size === 0)
|
|
42
|
+
return new Map();
|
|
43
|
+
const coll = await users.getCollection();
|
|
44
|
+
// Only fetch UserDisplay fields
|
|
45
|
+
const projection = Object.keys(UserDisplayFields).reduce((acc, field) => {
|
|
46
|
+
acc[field] = 1;
|
|
47
|
+
return acc;
|
|
48
|
+
}, {});
|
|
49
|
+
const result = await coll.find({ _id: { $in: Array.from(ids) } }, { projection }).toArray();
|
|
50
|
+
const map = new Map();
|
|
51
|
+
for (const u of result) {
|
|
52
|
+
map.set(u._id, {
|
|
53
|
+
_id: u._id,
|
|
54
|
+
avatar: u.avatar ?? undefined,
|
|
55
|
+
email: u.email,
|
|
56
|
+
first_name: u.first_name,
|
|
57
|
+
last_name: u.last_name,
|
|
58
|
+
phone: u.phone ?? undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return map;
|
|
62
|
+
}
|
|
63
|
+
export async function enrichUserRefs(doc) {
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
collectUserIds(doc, ids);
|
|
66
|
+
const map = await fetchUsersMap(ids);
|
|
67
|
+
return replaceRefs(doc, map);
|
|
68
|
+
}
|
|
69
|
+
export async function enrichUserRefsMany(docs) {
|
|
70
|
+
const ids = new Set();
|
|
71
|
+
for (const d of docs)
|
|
72
|
+
collectUserIds(d, ids);
|
|
73
|
+
const map = await fetchUsersMap(ids);
|
|
74
|
+
return docs.map(d => replaceRefs(d, map));
|
|
75
|
+
}
|
|
@@ -2,7 +2,6 @@ import { MongoCollectionClass } from '../../mongo-collection.js';
|
|
|
2
2
|
import { CreateUserDto, UpdateUserDto, User } from '@tmlmobilidade/types';
|
|
3
3
|
import { Filter, FindOptions, IndexDescription, WithId } from 'mongodb';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
type NewType = string;
|
|
6
5
|
declare class UsersClass extends MongoCollectionClass<User, CreateUserDto, UpdateUserDto> {
|
|
7
6
|
private static _instance;
|
|
8
7
|
protected createSchema: z.ZodSchema;
|
|
@@ -32,7 +31,7 @@ declare class UsersClass extends MongoCollectionClass<User, CreateUserDto, Updat
|
|
|
32
31
|
* @param includePasswordHash - Whether to include the password hash in the result
|
|
33
32
|
* @returns A promise that resolves to the matching user documents or null if not found
|
|
34
33
|
*/
|
|
35
|
-
findByOrganization(id:
|
|
34
|
+
findByOrganization(id: string, includePasswordHash?: boolean): Promise<{
|
|
36
35
|
created_at: number & {
|
|
37
36
|
__brand: "UnixTimestamp";
|
|
38
37
|
};
|
|
@@ -41,15 +40,15 @@ declare class UsersClass extends MongoCollectionClass<User, CreateUserDto, Updat
|
|
|
41
40
|
};
|
|
42
41
|
created_by?: string | undefined | undefined;
|
|
43
42
|
updated_by?: string | undefined | undefined;
|
|
44
|
-
phone?: string | null | undefined | undefined;
|
|
45
|
-
email: string;
|
|
46
|
-
permissions: import("@tmlmobilidade/types").Permission<unknown>[];
|
|
47
43
|
avatar?: string | null | undefined | undefined;
|
|
48
44
|
bio?: string | null | undefined | undefined;
|
|
45
|
+
email: string;
|
|
49
46
|
email_verified?: import("@tmlmobilidade/types").UnixTimestamp | null | undefined;
|
|
50
47
|
first_name: string;
|
|
51
48
|
last_name: string;
|
|
52
49
|
organization_id?: string | null | undefined | undefined;
|
|
50
|
+
permissions: import("@tmlmobilidade/types").Permission<unknown>[];
|
|
51
|
+
phone?: string | null | undefined | undefined;
|
|
53
52
|
preferences?: Record<string, Record<string, string | number | boolean | string[] | number[]>> | null | undefined;
|
|
54
53
|
role_ids: string[];
|
|
55
54
|
session_ids: string[];
|
|
@@ -72,15 +71,15 @@ declare class UsersClass extends MongoCollectionClass<User, CreateUserDto, Updat
|
|
|
72
71
|
};
|
|
73
72
|
created_by?: string | undefined | undefined;
|
|
74
73
|
updated_by?: string | undefined | undefined;
|
|
75
|
-
phone?: string | null | undefined | undefined;
|
|
76
|
-
email: string;
|
|
77
|
-
permissions: import("@tmlmobilidade/types").Permission<unknown>[];
|
|
78
74
|
avatar?: string | null | undefined | undefined;
|
|
79
75
|
bio?: string | null | undefined | undefined;
|
|
76
|
+
email: string;
|
|
80
77
|
email_verified?: import("@tmlmobilidade/types").UnixTimestamp | null | undefined;
|
|
81
78
|
first_name: string;
|
|
82
79
|
last_name: string;
|
|
83
80
|
organization_id?: string | null | undefined | undefined;
|
|
81
|
+
permissions: import("@tmlmobilidade/types").Permission<unknown>[];
|
|
82
|
+
phone?: string | null | undefined | undefined;
|
|
84
83
|
preferences?: Record<string, Record<string, string | number | boolean | string[] | number[]>> | null | undefined;
|
|
85
84
|
role_ids: string[];
|
|
86
85
|
session_ids: string[];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { MongoCollectionClass } from '../../mongo-collection.js';
|
|
3
3
|
import { UpdateUserSchema, UserSchema } from '@tmlmobilidade/types';
|
|
4
4
|
import { AsyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
5
|
+
/* * */
|
|
5
6
|
class UsersClass extends MongoCollectionClass {
|
|
6
7
|
static _instance;
|
|
7
8
|
createSchema = UserSchema;
|
|
@@ -13,6 +13,15 @@ declare class NotificationsClass extends MongoCollectionClass<Notification, Crea
|
|
|
13
13
|
protected getCollectionName(): string;
|
|
14
14
|
protected getCreateSchema(): z.ZodSchema;
|
|
15
15
|
protected getEnvName(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Collects all effective permissions of a user (direct + via roles),
|
|
18
|
+
* merging duplicates by (scope:action).
|
|
19
|
+
*/
|
|
20
|
+
private collectUserPermissions;
|
|
21
|
+
/**
|
|
22
|
+
* Determines whether a user can receive email notifications for a topic.
|
|
23
|
+
*/
|
|
24
|
+
private getNotificationPermission;
|
|
16
25
|
}
|
|
17
26
|
export declare const notifications: NotificationsClass;
|
|
18
27
|
export {};
|
|
@@ -3,7 +3,8 @@ import { MongoCollectionClass } from '../../mongo-collection.js';
|
|
|
3
3
|
import { sendNotificationEmail } from '@tmlmobilidade/emails';
|
|
4
4
|
import { getAppConfig } from '@tmlmobilidade/lib';
|
|
5
5
|
import { NotificationSchema, UpdateNotificationSchema } from '@tmlmobilidade/types';
|
|
6
|
-
import { AsyncSingletonProxy } from '@tmlmobilidade/utils';
|
|
6
|
+
import { AsyncSingletonProxy, mergeObjects } from '@tmlmobilidade/utils';
|
|
7
|
+
import { roles } from '../auth/roles.js';
|
|
7
8
|
import { users } from '../auth/users.js';
|
|
8
9
|
/* * */
|
|
9
10
|
class NotificationsClass extends MongoCollectionClass {
|
|
@@ -22,36 +23,49 @@ class NotificationsClass extends MongoCollectionClass {
|
|
|
22
23
|
return NotificationsClass._instance;
|
|
23
24
|
}
|
|
24
25
|
async sendNotification(scope, topic, user, id, title, description) {
|
|
25
|
-
|
|
26
|
+
// Fetch roles and users that have access to this topic
|
|
27
|
+
const rolesWithTopic = await roles.findMany({ 'permissions.action': topic });
|
|
28
|
+
const roleIdsWithTopic = rolesWithTopic.map(r => r._id);
|
|
29
|
+
const usersWithTopic = await users.findMany({
|
|
30
|
+
$or: [
|
|
31
|
+
{ 'permissions.action': topic },
|
|
32
|
+
{ role_ids: { $in: roleIdsWithTopic } },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
26
35
|
if (usersWithTopic.length === 0)
|
|
27
36
|
return;
|
|
28
|
-
|
|
37
|
+
// Base notification template
|
|
38
|
+
const baseNotification = {
|
|
29
39
|
created_by: user?._id,
|
|
30
40
|
is_read: false,
|
|
31
41
|
payload: {
|
|
32
42
|
body: description,
|
|
33
|
-
href: `${getAppConfig(
|
|
43
|
+
href: `${getAppConfig(scope, 'frontend_url')}/${scope}/${id}`,
|
|
34
44
|
icon: scope,
|
|
35
|
-
title
|
|
45
|
+
title,
|
|
36
46
|
},
|
|
37
47
|
priority: 'normal',
|
|
38
|
-
scope
|
|
39
|
-
topic
|
|
48
|
+
scope,
|
|
49
|
+
topic,
|
|
40
50
|
updated_by: user?._id,
|
|
41
51
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
52
|
+
// Iterate over eligible users (excluding creator)
|
|
53
|
+
for (const recipient of usersWithTopic.filter(u => u._id !== baseNotification.created_by)) {
|
|
54
|
+
const permissions = this.collectUserPermissions(recipient, rolesWithTopic);
|
|
55
|
+
const canReceiveEmail = this.getNotificationPermission(permissions, topic);
|
|
56
|
+
const newNotification = { ...baseNotification, user_id: recipient._id };
|
|
57
|
+
// Send email if permission allows
|
|
58
|
+
if (canReceiveEmail) {
|
|
46
59
|
await sendNotificationEmail({
|
|
47
60
|
props: {
|
|
48
|
-
body:
|
|
49
|
-
href:
|
|
50
|
-
priority:
|
|
51
|
-
scope:
|
|
52
|
-
title:
|
|
53
|
-
topic:
|
|
54
|
-
},
|
|
61
|
+
body: baseNotification.payload.body,
|
|
62
|
+
href: baseNotification.payload.href ?? '',
|
|
63
|
+
priority: baseNotification.priority,
|
|
64
|
+
scope: baseNotification.scope,
|
|
65
|
+
title: baseNotification.payload.title,
|
|
66
|
+
topic: baseNotification.topic,
|
|
67
|
+
},
|
|
68
|
+
to: recipient.email,
|
|
55
69
|
});
|
|
56
70
|
}
|
|
57
71
|
await notifications.insertOne(newNotification);
|
|
@@ -71,5 +85,29 @@ class NotificationsClass extends MongoCollectionClass {
|
|
|
71
85
|
getEnvName() {
|
|
72
86
|
return 'DATABASE_URI';
|
|
73
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Collects all effective permissions of a user (direct + via roles),
|
|
90
|
+
* merging duplicates by (scope:action).
|
|
91
|
+
*/
|
|
92
|
+
collectUserPermissions(user, rolesWithTopic) {
|
|
93
|
+
const rolePermissions = rolesWithTopic
|
|
94
|
+
.filter(role => user.role_ids?.includes(role._id))
|
|
95
|
+
.flatMap(role => role.permissions ?? []);
|
|
96
|
+
const allPermissions = [...(user.permissions ?? []), ...rolePermissions];
|
|
97
|
+
const map = new Map();
|
|
98
|
+
for (const permission of allPermissions) {
|
|
99
|
+
const key = `${permission.scope}:${permission.action}`;
|
|
100
|
+
const existing = map.get(key);
|
|
101
|
+
map.set(key, existing ? mergeObjects(existing, permission) : permission);
|
|
102
|
+
}
|
|
103
|
+
return map;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Determines whether a user can receive email notifications for a topic.
|
|
107
|
+
*/
|
|
108
|
+
getNotificationPermission(permissions, topic) {
|
|
109
|
+
const permission = permissions.get(`notifications:${topic}`);
|
|
110
|
+
return permission?.resource?.send_mail ?? false;
|
|
111
|
+
}
|
|
74
112
|
}
|
|
75
113
|
export const notifications = AsyncSingletonProxy(NotificationsClass);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { AggregationPipeline } from './aggregation-pipeline.js';
|
|
1
2
|
import { MongoConnector } from '@tmlmobilidade/connectors';
|
|
2
3
|
import { type UnixTimestamp } from '@tmlmobilidade/types';
|
|
3
4
|
import { AggregateOptions, AggregationCursor, Collection, DeleteOptions, DeleteResult, Document, Filter, FindOptions, IndexDescription, InsertManyResult, InsertOneOptions, InsertOneResult, MongoClientOptions, UpdateOptions, UpdateResult, WithId } from 'mongodb';
|
|
4
5
|
import { z } from 'zod';
|
|
5
|
-
import { AggregationPipeline } from './aggregation-pipeline.js';
|
|
6
6
|
export declare abstract class MongoCollectionClass<T extends Document, TCreate, TUpdate> {
|
|
7
7
|
protected createSchema: null | z.ZodSchema;
|
|
8
8
|
protected mongoCollection: Collection<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmlmobilidade/interfaces",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20251008.1352.27-staging.0",
|
|
4
4
|
"author": "João de Vasconcelos & Jusi Monteiro",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://github.com/tmlmobilidade/services#readme",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"lint:fix": "eslint --fix"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@aws-sdk/client-s3": "3.
|
|
40
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
39
|
+
"@aws-sdk/client-s3": "3.901.0",
|
|
40
|
+
"@aws-sdk/s3-request-presigner": "3.901.0",
|
|
41
41
|
"@tmlmobilidade/connectors": "*",
|
|
42
42
|
"@tmlmobilidade/emails": "*",
|
|
43
43
|
"@tmlmobilidade/lib": "*",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@carrismetropolitana/eslint": "20250622.1204.50",
|
|
56
56
|
"@types/luxon": "3.7.1",
|
|
57
|
-
"@types/node": "24.
|
|
57
|
+
"@types/node": "24.7.0",
|
|
58
58
|
"resolve-tspaths": "0.8.23",
|
|
59
59
|
"rimraf": "6.0.1",
|
|
60
60
|
"typescript": "5.9.3"
|