@lilaquadrat/studio 10.10.0 → 11.1.1
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/lib/models.d.ts +6 -1
- package/lib/models.js +4 -1
- package/lib/models.js.map +1 -1
- package/lib/services.d.ts +4 -1
- package/lib/services.js +4 -1
- package/lib/services.js.map +1 -1
- package/lib/src/interfaces/Participant.d.ts +62 -0
- package/lib/src/interfaces/Participant.js +4 -0
- package/lib/src/interfaces/Participant.js.map +1 -0
- package/lib/src/models/conversation.model.js +30 -0
- package/lib/src/models/conversation.model.js.map +1 -1
- package/lib/src/models/license.model.d.ts +4 -0
- package/lib/src/models/license.model.js +70 -0
- package/lib/src/models/license.model.js.map +1 -0
- package/lib/src/models/participant.model.d.ts +4 -0
- package/lib/src/models/participant.model.js +50 -0
- package/lib/src/models/participant.model.js.map +1 -0
- package/lib/src/models/permissions.model.js +3 -0
- package/lib/src/models/permissions.model.js.map +1 -1
- package/lib/src/models/storage.model.js +44 -0
- package/lib/src/models/storage.model.js.map +1 -1
- package/lib/src/models/upload.model.js +34 -0
- package/lib/src/models/upload.model.js.map +1 -1
- package/lib/src/services/memory-cache.service.d.ts +25 -0
- package/lib/src/services/memory-cache.service.js +59 -0
- package/lib/src/services/memory-cache.service.js.map +1 -0
- package/lib/src/services/participants.service.d.ts +60 -0
- package/lib/src/services/participants.service.js +147 -0
- package/lib/src/services/participants.service.js.map +1 -0
- package/lib/src/services/storage.service.d.ts +17 -5
- package/lib/src/services/storage.service.js +56 -23
- package/lib/src/services/storage.service.js.map +1 -1
- package/lib/src/services/support-access.service.d.ts +35 -0
- package/lib/src/services/support-access.service.js +113 -0
- package/lib/src/services/support-access.service.js.map +1 -0
- package/lib/src/services/upload.service.d.ts +5 -1
- package/lib/src/services/upload.service.js +11 -6
- package/lib/src/services/upload.service.js.map +1 -1
- package/lib/tests/storageServiceToken.spec.js +14 -0
- package/lib/tests/storageServiceToken.spec.js.map +1 -1
- package/lib/tests/uploadServiceCreate.spec.js +19 -0
- package/lib/tests/uploadServiceCreate.spec.js.map +1 -1
- package/package.json +2 -7
- package/lib/frontend.d.ts +0 -8
- package/lib/frontend.js +0 -9
- package/lib/frontend.js.map +0 -1
- package/lib/src/filterMergeModules.d.ts +0 -3
- package/lib/src/filterMergeModules.js +0 -29
- package/lib/src/filterMergeModules.js.map +0 -1
- package/lib/src/prepareContent.d.ts +0 -7
- package/lib/src/prepareContent.js +0 -81
- package/lib/src/prepareContent.js.map +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logger from '../logger.js';
|
|
2
|
+
class MemoryCache {
|
|
3
|
+
static clear() {
|
|
4
|
+
MemoryCache.cache = {};
|
|
5
|
+
}
|
|
6
|
+
static remove(cacheKeyOptions) {
|
|
7
|
+
delete MemoryCache.cache[MemoryCache.getCacheKey(cacheKeyOptions)];
|
|
8
|
+
}
|
|
9
|
+
static clearUser(user) {
|
|
10
|
+
const suffix = `:${user}`;
|
|
11
|
+
Object.keys(MemoryCache.cache).forEach(key => {
|
|
12
|
+
if (key.endsWith(suffix)) {
|
|
13
|
+
delete MemoryCache.cache[key];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
static clearUsers(users) {
|
|
18
|
+
users.forEach((single) => MemoryCache.clearUser(single));
|
|
19
|
+
}
|
|
20
|
+
static upsert(cacheKey, data, lifetime) {
|
|
21
|
+
MemoryCache.cache[cacheKey.toString()] = {
|
|
22
|
+
data,
|
|
23
|
+
...(lifetime ? { expiresAt: Date.now() + lifetime } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
static get(cacheKey) {
|
|
27
|
+
const entry = MemoryCache.cache[cacheKey.toString()];
|
|
28
|
+
if (!entry)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (entry.expiresAt && entry.expiresAt < Date.now()) {
|
|
31
|
+
delete MemoryCache.cache[cacheKey.toString()];
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return entry.data;
|
|
35
|
+
}
|
|
36
|
+
static async use(cacheKeyOptions, fetchFunction) {
|
|
37
|
+
const cacheKey = MemoryCache.getCacheKey(cacheKeyOptions);
|
|
38
|
+
const cached = MemoryCache.get(cacheKey);
|
|
39
|
+
if (cached) {
|
|
40
|
+
logger.debug('cache.hit', { key: cacheKey });
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
logger.debug('cache.miss', { key: cacheKey });
|
|
44
|
+
const fetchedData = await fetchFunction;
|
|
45
|
+
MemoryCache.upsert(cacheKey, fetchedData, cacheKeyOptions.lifetime);
|
|
46
|
+
return fetchedData;
|
|
47
|
+
}
|
|
48
|
+
static getCacheKey(options) {
|
|
49
|
+
const keyArray = [options.type];
|
|
50
|
+
if (options.id)
|
|
51
|
+
keyArray.push(options.id.toString());
|
|
52
|
+
if (options.user)
|
|
53
|
+
keyArray.push(options.user.toString());
|
|
54
|
+
return keyArray.join(':');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
MemoryCache.cache = {};
|
|
58
|
+
export default MemoryCache;
|
|
59
|
+
//# sourceMappingURL=memory-cache.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-cache.service.js","sourceRoot":"","sources":["../../../src/services/memory-cache.service.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,cAAc,CAAC;AAalC,MAAM,WAAW;IAIf,MAAM,CAAC,KAAK;QAEV,WAAW,CAAC,KAAK,GAAG,EAAE,CAAC;IAEzB,CAAC;IAED,MAAM,CAAC,MAAM,CAAC,eAAgC;QAE5C,OAAO,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,CAAC;IAErE,CAAC;IAED,MAAM,CAAC,SAAS,CAAC,IAAY;QAE3B,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QAC1B,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;YAE3C,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAEzB,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAEhC,CAAC;QAEH,CAAC,CAAC,CAAC;IAEL,CAAC;IAED,MAAM,CAAC,UAAU,CAAC,KAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAE3D,CAAC;IAED,MAAM,CAAC,MAAM,CAAC,QAAgB,EAAE,IAAa,EAAE,QAAiB;QAE9D,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG;YACvC,IAAI;YACJ,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1D,CAAC;IAEJ,CAAC;IAED,MAAM,CAAC,GAAG,CAAC,QAAgB;QAEzB,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;QAErD,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAE7B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAEpD,OAAO,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC9C,OAAO,SAAS,CAAC;QAEnB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC;IAEpB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,GAAG,CAAI,eAAgC,EAAE,aAAyB;QAE7E,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,IAAI,MAAM,EAAE,CAAC;YAEX,MAAM,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC7C,OAAO,MAAW,CAAC;QAErB,CAAC;QAED,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE9C,MAAM,WAAW,GAAG,MAAM,aAAa,CAAC;QAExC,WAAW,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;QACpE,OAAO,WAAW,CAAC;IAErB,CAAC;IAED,MAAM,CAAC,WAAW,CAAC,OAAwB;QAEzC,MAAM,QAAQ,GAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,OAAO,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrD,IAAI,OAAO,CAAC,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEzD,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE5B,CAAC;;AAzFM,iBAAK,GAA+B,EAAE,CAAC;AA4FhD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb';
|
|
2
|
+
import Immutable from '../Immutable.class.js';
|
|
3
|
+
import Participant, { ParticipantRole, ParticipantState, Right } from '../interfaces/Participant.js';
|
|
4
|
+
/**
|
|
5
|
+
* participant collection and conversation access.
|
|
6
|
+
*
|
|
7
|
+
* The base service is shared by studio-chat (which extends it with conversation bookkeeping
|
|
8
|
+
* that needs the message model) and by studio-media (which uses it for the conversation
|
|
9
|
+
* attachment access checks). Access rules therefore exist exactly once.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ParticipantsService extends Immutable<Participant> {
|
|
12
|
+
model: import("../classes/modelv2.class.js").default<Participant, "create" | "changeState">;
|
|
13
|
+
addToConversation(participant: string, conversation: ObjectId, user: string, rights?: Right[], role?: ParticipantRole): Promise<import("@lilaquadrat/interfaces").CreateResult<import("@lilaquadrat/interfaces").BasicData<Participant>>>;
|
|
14
|
+
updateLastReadId(participant: string, conversation: ObjectId, lastReadId: number): Promise<import("mongodb").UpdateResult<Participant>>;
|
|
15
|
+
/**
|
|
16
|
+
* silence a conversation for one participant. Only ever touches the caller's own doc, so
|
|
17
|
+
* membership stays untouched — no refreshParticipants/refreshParticipantsHash needed.
|
|
18
|
+
*
|
|
19
|
+
* Unmuting $unsets instead of writing `false`: absent and false mean the same thing to
|
|
20
|
+
* every reader, and the docs of the (vast) majority who never mute stay clean.
|
|
21
|
+
*/
|
|
22
|
+
setMuted(conversation: ObjectId, participant: string, muted: boolean): Promise<import("mongodb").UpdateResult<Participant>>;
|
|
23
|
+
updateLastestMessageId(participant: string, conversation: ObjectId, latestMessageId: number): Promise<import("mongodb").UpdateResult<Participant>>;
|
|
24
|
+
getByConversationAndUser(conversationId: ObjectId, user: string): Promise<import("mongodb").WithId<Participant> | null>;
|
|
25
|
+
/**
|
|
26
|
+
* mark participants as support team members. Separate from adding them because they are added
|
|
27
|
+
* through updateParticipants(), which is deliberately generic — and it also has to reach docs
|
|
28
|
+
* that already existed (a member who left and comes back with the team).
|
|
29
|
+
*/
|
|
30
|
+
markSupportAgents(conversationId: ObjectId, users: string[]): Promise<void> | Promise<import("mongodb").UpdateResult<Participant>>;
|
|
31
|
+
/** the support team members of a conversation — who may see its system messages */
|
|
32
|
+
getSupportAgents(conversationId: ObjectId): Promise<string[]>;
|
|
33
|
+
/**
|
|
34
|
+
* all participant docs of a conversation (incl. their rights & state), excluding deleted.
|
|
35
|
+
*/
|
|
36
|
+
listByConversation(conversationId: ObjectId): Promise<import("mongodb").WithId<Participant>[]>;
|
|
37
|
+
setRights(conversationId: ObjectId, target: string, rights: Right[]): Promise<import("mongodb").UpdateResult<Participant>>;
|
|
38
|
+
/**
|
|
39
|
+
* flip a participant's own state. Written for the per-participant delete ('deleted'), which
|
|
40
|
+
* hides the conversation from that one user's list without touching anybody else's copy —
|
|
41
|
+
* conversations.service list() and listByConversation() both already filter
|
|
42
|
+
* `state: { $ne: 'deleted' }`, so the read side needs no change.
|
|
43
|
+
*
|
|
44
|
+
* Callers moving a participant OUT of 'active' have to refresh participants/participantsHash
|
|
45
|
+
* themselves (leave() and block() do). The delete route leaves first, so the state is never
|
|
46
|
+
* 'active' by the time this runs.
|
|
47
|
+
*/
|
|
48
|
+
setState(conversationId: ObjectId, target: string, state: ParticipantState): Promise<import("mongodb").UpdateResult<Participant>>;
|
|
49
|
+
/**
|
|
50
|
+
* membership plus the named rights on a conversation. The single access chokepoint for both
|
|
51
|
+
* transports (REST routes and websocket actions) and for media's conversation attachment
|
|
52
|
+
* endpoints.
|
|
53
|
+
*/
|
|
54
|
+
checkAccess(requiredAnyStatus: string[], requiredRights: string[] | undefined, settings: {
|
|
55
|
+
conversationId: ObjectId;
|
|
56
|
+
user: string;
|
|
57
|
+
}): Promise<boolean>;
|
|
58
|
+
}
|
|
59
|
+
declare const _default: ParticipantsService;
|
|
60
|
+
export default _default;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Immutable from '../Immutable.class.js';
|
|
2
|
+
import ParticipantModel from '../models/participant.model.js';
|
|
3
|
+
import ConversationModel from '../models/conversation.model.js';
|
|
4
|
+
import MemoryCache from './memory-cache.service.js';
|
|
5
|
+
import supportAccess from './support-access.service.js';
|
|
6
|
+
/**
|
|
7
|
+
* conversation and participant docs are cached per process; callers that mutate them
|
|
8
|
+
* (studio-chat) invalidate explicitly, but a service that only reads (studio-media) cannot,
|
|
9
|
+
* so entries age out instead of holding revoked access forever.
|
|
10
|
+
*/
|
|
11
|
+
const ACCESS_CACHE_LIFETIME_MS = 30000;
|
|
12
|
+
/**
|
|
13
|
+
* participant collection and conversation access.
|
|
14
|
+
*
|
|
15
|
+
* The base service is shared by studio-chat (which extends it with conversation bookkeeping
|
|
16
|
+
* that needs the message model) and by studio-media (which uses it for the conversation
|
|
17
|
+
* attachment access checks). Access rules therefore exist exactly once.
|
|
18
|
+
*/
|
|
19
|
+
export class ParticipantsService extends Immutable {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.model = ParticipantModel;
|
|
23
|
+
}
|
|
24
|
+
addToConversation(participant, conversation, user, rights = ['read', 'write', 'files'], role) {
|
|
25
|
+
return this.create({
|
|
26
|
+
user: participant,
|
|
27
|
+
conversation,
|
|
28
|
+
state: 'active',
|
|
29
|
+
rights,
|
|
30
|
+
lastReadId: 0,
|
|
31
|
+
...(role ? { role } : {})
|
|
32
|
+
}, String(user), 'chat', undefined, undefined, undefined, {
|
|
33
|
+
returnDocument: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
updateLastReadId(participant, conversation, lastReadId) {
|
|
37
|
+
return this.model.db.updateOne({
|
|
38
|
+
user: participant,
|
|
39
|
+
conversation
|
|
40
|
+
}, { $max: { lastReadId } });
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* silence a conversation for one participant. Only ever touches the caller's own doc, so
|
|
44
|
+
* membership stays untouched — no refreshParticipants/refreshParticipantsHash needed.
|
|
45
|
+
*
|
|
46
|
+
* Unmuting $unsets instead of writing `false`: absent and false mean the same thing to
|
|
47
|
+
* every reader, and the docs of the (vast) majority who never mute stay clean.
|
|
48
|
+
*/
|
|
49
|
+
setMuted(conversation, participant, muted) {
|
|
50
|
+
return this.model.db.updateOne({
|
|
51
|
+
user: participant,
|
|
52
|
+
conversation
|
|
53
|
+
}, muted ? { $set: { muted: true } } : { $unset: { muted: '' } });
|
|
54
|
+
}
|
|
55
|
+
updateLastestMessageId(participant, conversation, latestMessageId) {
|
|
56
|
+
return this.model.db.updateOne({
|
|
57
|
+
user: participant,
|
|
58
|
+
conversation
|
|
59
|
+
}, { $set: { latestMessageId } });
|
|
60
|
+
}
|
|
61
|
+
getByConversationAndUser(conversationId, user) {
|
|
62
|
+
return this.model.db.findOne({ conversation: conversationId, user });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* mark participants as support team members. Separate from adding them because they are added
|
|
66
|
+
* through updateParticipants(), which is deliberately generic — and it also has to reach docs
|
|
67
|
+
* that already existed (a member who left and comes back with the team).
|
|
68
|
+
*/
|
|
69
|
+
markSupportAgents(conversationId, users) {
|
|
70
|
+
if (!users.length)
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
return this.model.db.updateMany({ conversation: conversationId, user: { $in: users } }, { $set: { role: 'agent' } });
|
|
73
|
+
}
|
|
74
|
+
/** the support team members of a conversation — who may see its system messages */
|
|
75
|
+
async getSupportAgents(conversationId) {
|
|
76
|
+
const agents = await this.model.db
|
|
77
|
+
.find({ conversation: conversationId, role: 'agent' }, { projection: { user: 1 } })
|
|
78
|
+
.toArray();
|
|
79
|
+
return agents.map((single) => single.user);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* all participant docs of a conversation (incl. their rights & state), excluding deleted.
|
|
83
|
+
*/
|
|
84
|
+
listByConversation(conversationId) {
|
|
85
|
+
return this.model.db.find({ conversation: conversationId, state: { $ne: 'deleted' } }).toArray();
|
|
86
|
+
}
|
|
87
|
+
setRights(conversationId, target, rights) {
|
|
88
|
+
return this.model.db.updateOne({ conversation: conversationId, user: target }, { $set: { rights } });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* flip a participant's own state. Written for the per-participant delete ('deleted'), which
|
|
92
|
+
* hides the conversation from that one user's list without touching anybody else's copy —
|
|
93
|
+
* conversations.service list() and listByConversation() both already filter
|
|
94
|
+
* `state: { $ne: 'deleted' }`, so the read side needs no change.
|
|
95
|
+
*
|
|
96
|
+
* Callers moving a participant OUT of 'active' have to refresh participants/participantsHash
|
|
97
|
+
* themselves (leave() and block() do). The delete route leaves first, so the state is never
|
|
98
|
+
* 'active' by the time this runs.
|
|
99
|
+
*/
|
|
100
|
+
setState(conversationId, target, state) {
|
|
101
|
+
return this.model.db.updateOne({ conversation: conversationId, user: target }, { $set: { state } });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* membership plus the named rights on a conversation. The single access chokepoint for both
|
|
105
|
+
* transports (REST routes and websocket actions) and for media's conversation attachment
|
|
106
|
+
* endpoints.
|
|
107
|
+
*/
|
|
108
|
+
async checkAccess(requiredAnyStatus, requiredRights, settings) {
|
|
109
|
+
const { conversationId, user } = settings;
|
|
110
|
+
// Fetch both in parallel with distinct cache keys to avoid collisions
|
|
111
|
+
const [conversation, participant] = await Promise.all([
|
|
112
|
+
MemoryCache.use({ type: "conversation", id: conversationId, lifetime: ACCESS_CACHE_LIFETIME_MS }, ConversationModel.db.findOne({ _id: conversationId })),
|
|
113
|
+
MemoryCache.use({ type: "participant", id: conversationId, user, lifetime: ACCESS_CACHE_LIFETIME_MS }, this.getByConversationAndUser(conversationId, user))
|
|
114
|
+
]);
|
|
115
|
+
if (!participant)
|
|
116
|
+
return false;
|
|
117
|
+
if (!conversation)
|
|
118
|
+
return false;
|
|
119
|
+
/**
|
|
120
|
+
* Support staff revocation.
|
|
121
|
+
*
|
|
122
|
+
* The team reconcile is add-only by design (addMissingSupportMembers): nothing on a support
|
|
123
|
+
* conversation records who the customer is, so "remove everyone not on the team" could eject
|
|
124
|
+
* the customer themselves. The consequence is that taking someone's `support:member` scope
|
|
125
|
+
* away never removed their participant docs — without this check they would keep reading
|
|
126
|
+
* every support conversation they were ever added to, forever.
|
|
127
|
+
*
|
|
128
|
+
* `role: 'agent'` is only ever set on support participants (participantsService
|
|
129
|
+
* .markSupportAgents), so this touches nobody else — least of all a support agent's own
|
|
130
|
+
* support conversation, where they are the customer and carry no role.
|
|
131
|
+
*/
|
|
132
|
+
if (conversation.operationMode === 'support' && participant.role === 'agent' && !await supportAccess.isStaff(user))
|
|
133
|
+
return false;
|
|
134
|
+
// Validate participant membership (cheap array includes)
|
|
135
|
+
if (!conversation.participants?.includes(user) && !requiredAnyStatus.some((single) => ['left', 'removed'].includes(single)))
|
|
136
|
+
return false;
|
|
137
|
+
// Status check
|
|
138
|
+
if (!requiredAnyStatus.includes(participant.state))
|
|
139
|
+
return false;
|
|
140
|
+
// Rights check (all required must be present)
|
|
141
|
+
if (requiredRights && !requiredRights.every(r => participant.rights?.includes(r)))
|
|
142
|
+
return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export default new ParticipantsService();
|
|
147
|
+
//# sourceMappingURL=participants.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"participants.service.js","sourceRoot":"","sources":["../../../src/services/participants.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,7 @@ export declare class StorageService extends Immutable<Storage> {
|
|
|
63
63
|
prefix?: string;
|
|
64
64
|
customer?: string;
|
|
65
65
|
list?: string;
|
|
66
|
+
conversation?: string;
|
|
66
67
|
}): {
|
|
67
68
|
token: string;
|
|
68
69
|
expiresIn: number;
|
|
@@ -107,6 +108,7 @@ export declare class StorageService extends Immutable<Storage> {
|
|
|
107
108
|
prefix?: string;
|
|
108
109
|
customer?: string;
|
|
109
110
|
list?: string;
|
|
111
|
+
conversation?: string;
|
|
110
112
|
}): boolean;
|
|
111
113
|
/**
|
|
112
114
|
* Creates a secure download URL with time-limited token for a storage file.
|
|
@@ -132,11 +134,14 @@ export declare class StorageService extends Immutable<Storage> {
|
|
|
132
134
|
app?: string;
|
|
133
135
|
url?: string;
|
|
134
136
|
prefix?: string;
|
|
137
|
+
conversation?: string;
|
|
135
138
|
}): Promise<{
|
|
136
139
|
url: string;
|
|
137
140
|
expiresAt: number;
|
|
138
141
|
}>;
|
|
139
|
-
handleFile(uploadInternalId: ObjectId, index: number, file: MultipartFile | undefined
|
|
142
|
+
handleFile(uploadInternalId: ObjectId, index: number, file: MultipartFile | undefined, options?: {
|
|
143
|
+
maxSize?: number;
|
|
144
|
+
}): Promise<import("@lilaquadrat/interfaces").Stored<Upload>>;
|
|
140
145
|
complete(upload: Upload, options?: {
|
|
141
146
|
useNode?: boolean;
|
|
142
147
|
}): Promise<StorageFilePaths>;
|
|
@@ -196,8 +201,9 @@ export declare class StorageService extends Immutable<Storage> {
|
|
|
196
201
|
* checks if the file was already successfully uploaded
|
|
197
202
|
*/
|
|
198
203
|
exists(filename: string, prefix: string | undefined, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: {
|
|
199
|
-
company
|
|
200
|
-
project
|
|
204
|
+
company?: string;
|
|
205
|
+
project?: string | undefined;
|
|
206
|
+
conversation?: ObjectId;
|
|
201
207
|
}): Promise<boolean>;
|
|
202
208
|
/**
|
|
203
209
|
* counts non-versioned storage documents bound to a specific assetId
|
|
@@ -210,13 +216,19 @@ export declare class StorageService extends Immutable<Storage> {
|
|
|
210
216
|
checkAllowedSize(filesize: number, allowedSize: number): boolean;
|
|
211
217
|
stats(company: string, project: string, customer?: ObjectId): Promise<MediaStats>;
|
|
212
218
|
getByInternalId(internalId: ObjectId, options: Required<Options>): Promise<BasicData<Storage> | null>;
|
|
213
|
-
getPathByFilename(filename: string, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options
|
|
214
|
-
|
|
219
|
+
getPathByFilename(filename: string, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options & {
|
|
220
|
+
conversation?: ObjectId;
|
|
221
|
+
}): Promise<WithId<Storage> | null>;
|
|
222
|
+
getByFilename(filename: string, prefix: string | undefined, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options & {
|
|
223
|
+
conversation?: ObjectId;
|
|
224
|
+
}): Promise<BasicData<Storage> | null>;
|
|
215
225
|
getFilesNotInPaths(paths: string[], app: string, company: string, project: string): Promise<WithId<Storage>[]>;
|
|
216
226
|
removeBlobMembers(internalId: ObjectId, customer: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
217
227
|
removeBlob(internalId: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
228
|
+
removeBlobConversation(internalId: ObjectId, conversation: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
218
229
|
removeBlobViaPath(storage: WithId<Storage>[], options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
219
230
|
removeBlobVersion(parentId: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
231
|
+
removeConversationBlobVersion(parentId: ObjectId, conversation: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
|
|
220
232
|
}
|
|
221
233
|
declare const _default: StorageService;
|
|
222
234
|
export default _default;
|
|
@@ -205,6 +205,10 @@ export class StorageService extends Immutable {
|
|
|
205
205
|
console.debug('[validateSecureToken] list mismatch', { token: tokenLimits.list, provided: checkLimits.list });
|
|
206
206
|
return false;
|
|
207
207
|
}
|
|
208
|
+
if (tokenLimits.conversation && tokenLimits.conversation !== checkLimits.conversation) {
|
|
209
|
+
console.debug('[validateSecureToken] conversation mismatch', { token: tokenLimits.conversation, provided: checkLimits.conversation });
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
208
212
|
// For URL limit, check if requested URL starts with the token's allowed prefix
|
|
209
213
|
if (tokenLimits.url && checkLimits.url && !checkLimits.url.startsWith(tokenLimits.url)) {
|
|
210
214
|
console.debug('[validateSecureToken] url mismatch', { tokenUrl: tokenLimits.url, providedUrl: checkLimits.url });
|
|
@@ -247,6 +251,7 @@ export class StorageService extends Immutable {
|
|
|
247
251
|
project: limits?.project || storage.project,
|
|
248
252
|
app: limits?.app || storage.app,
|
|
249
253
|
prefix: limits?.prefix || storage.prefix,
|
|
254
|
+
conversation: limits?.conversation || storage.conversation?.toString(),
|
|
250
255
|
url: limits?.url,
|
|
251
256
|
};
|
|
252
257
|
// Generate secure token
|
|
@@ -254,13 +259,16 @@ export class StorageService extends Immutable {
|
|
|
254
259
|
// Construct download URL
|
|
255
260
|
const baseUrl = process.env.SECURE_URL;
|
|
256
261
|
const path = storage.prefix ? `${storage.prefix}/${storage.filename}` : storage.filename;
|
|
257
|
-
const
|
|
262
|
+
const scope = storage.company
|
|
263
|
+
? `${storage.app}/${storage.company}/${storage.project}`
|
|
264
|
+
: `${storage.app}/${storage.conversation}`;
|
|
265
|
+
const url = `${baseUrl}/${scope}/${path}?token=${tokenData.token}`;
|
|
258
266
|
return {
|
|
259
267
|
url,
|
|
260
268
|
expiresAt: tokenData.expiresAt,
|
|
261
269
|
};
|
|
262
270
|
}
|
|
263
|
-
async handleFile(uploadInternalId, index, file) {
|
|
271
|
+
async handleFile(uploadInternalId, index, file, options) {
|
|
264
272
|
if (!file)
|
|
265
273
|
throw new Error('FILE_MISSING');
|
|
266
274
|
/**
|
|
@@ -278,26 +286,20 @@ export class StorageService extends Immutable {
|
|
|
278
286
|
await fs.promises.unlink(path.file);
|
|
279
287
|
throw new Error('FILE_SIZE');
|
|
280
288
|
}
|
|
281
|
-
// this is not correct.
|
|
282
|
-
// the idea is, the development does not work like that.
|
|
283
|
-
// needs to be reintroduced
|
|
284
|
-
// if (uploadFile.size) {
|
|
285
|
-
//
|
|
286
|
-
// const chunkStat = await fs.promises.stat(path.file);
|
|
287
|
-
// const existingSize = uploadFile.paths?.length
|
|
288
|
-
// ? (await Promise.all(uploadFile.paths.map((p) => fs.promises.stat(p.path).then((s) => s.size).catch(() => 0))))
|
|
289
|
-
// .reduce((sum, s) => sum + s, 0)
|
|
290
|
-
// : 0;
|
|
291
|
-
// if (existingSize + chunkStat.size > uploadFile.size) {
|
|
292
|
-
// await fs.promises.unlink(path.file);
|
|
293
|
-
// throw new Error('FILE_SIZE');
|
|
294
|
-
// }
|
|
295
|
-
// }
|
|
296
289
|
const updloadState = await UploadService.addPathToUpload(uploadInternalId, index, path.file);
|
|
297
290
|
/**
|
|
298
291
|
* if all chunks are there, complete the file and the upload
|
|
299
292
|
*/
|
|
300
293
|
if (updloadState.chunks === updloadState.paths?.length) {
|
|
294
|
+
if (options?.maxSize) {
|
|
295
|
+
const sizes = await Promise.all((updloadState.paths ?? [])
|
|
296
|
+
.map((single) => fs.promises.stat(single.path).then((stat) => stat.size).catch(() => 0)));
|
|
297
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
298
|
+
if (total > options.maxSize) {
|
|
299
|
+
await this.removeUploadFiles(updloadState);
|
|
300
|
+
throw new Error('FILE_SIZE');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
301
303
|
await UploadService.setState(updloadState._id, 'waiting');
|
|
302
304
|
await JetStreamService.publish('actions.files.uploaded', updloadState);
|
|
303
305
|
}
|
|
@@ -903,16 +905,20 @@ export class StorageService extends Immutable {
|
|
|
903
905
|
*/
|
|
904
906
|
exists(filename, prefix, app, customer, list, options) {
|
|
905
907
|
const query = {
|
|
906
|
-
company: options.company,
|
|
907
|
-
project: options.project,
|
|
908
908
|
filename,
|
|
909
909
|
prefix,
|
|
910
910
|
app,
|
|
911
911
|
};
|
|
912
|
+
if (options.company)
|
|
913
|
+
query.company = options.company;
|
|
914
|
+
if (options.project)
|
|
915
|
+
query.project = options.project;
|
|
912
916
|
if (customer)
|
|
913
917
|
query.customer = customer;
|
|
914
918
|
if (list)
|
|
915
919
|
query.list = list;
|
|
920
|
+
if (options.conversation)
|
|
921
|
+
query.conversation = options.conversation;
|
|
916
922
|
return StorageModel.db.countDocuments(query)
|
|
917
923
|
.then((exists) => exists >= 1);
|
|
918
924
|
}
|
|
@@ -992,14 +998,18 @@ export class StorageService extends Immutable {
|
|
|
992
998
|
const query = {
|
|
993
999
|
filename: filenameObject.filename,
|
|
994
1000
|
prefix: filenameObject.directory,
|
|
995
|
-
company: options.company,
|
|
996
|
-
project: options.project,
|
|
997
1001
|
app,
|
|
998
1002
|
};
|
|
1003
|
+
if (options.company)
|
|
1004
|
+
query.company = options.company;
|
|
1005
|
+
if (options.project)
|
|
1006
|
+
query.project = options.project;
|
|
999
1007
|
if (customer)
|
|
1000
1008
|
query.customer = customer;
|
|
1001
1009
|
if (list)
|
|
1002
1010
|
query.list = list;
|
|
1011
|
+
if (options.conversation)
|
|
1012
|
+
query.conversation = options.conversation;
|
|
1003
1013
|
return StorageModel.db.findOne(query, {
|
|
1004
1014
|
projection: { path: 1, metadata: 1 },
|
|
1005
1015
|
});
|
|
@@ -1008,14 +1018,18 @@ export class StorageService extends Immutable {
|
|
|
1008
1018
|
const query = {
|
|
1009
1019
|
filename,
|
|
1010
1020
|
prefix,
|
|
1011
|
-
company: options.company,
|
|
1012
|
-
project: options.project,
|
|
1013
1021
|
app,
|
|
1014
1022
|
};
|
|
1023
|
+
if (options.company)
|
|
1024
|
+
query.company = options.company;
|
|
1025
|
+
if (options.project)
|
|
1026
|
+
query.project = options.project;
|
|
1015
1027
|
if (customer)
|
|
1016
1028
|
query.customer = customer;
|
|
1017
1029
|
if (list)
|
|
1018
1030
|
query.list = list;
|
|
1031
|
+
if (options.conversation)
|
|
1032
|
+
query.conversation = options.conversation;
|
|
1019
1033
|
return StorageModel.db.findOne(query);
|
|
1020
1034
|
}
|
|
1021
1035
|
getFilesNotInPaths(paths, app, company, project) {
|
|
@@ -1051,6 +1065,15 @@ export class StorageService extends Immutable {
|
|
|
1051
1065
|
await this.delete(internalId, options.user, options.app);
|
|
1052
1066
|
await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
|
|
1053
1067
|
}
|
|
1068
|
+
async removeBlobConversation(internalId, conversation, options) {
|
|
1069
|
+
const storage = await this.model.db.findOne({ _id: internalId, app: 'conversations', conversation });
|
|
1070
|
+
if (!storage)
|
|
1071
|
+
throw new Error('BLOB_NOT_FOUND');
|
|
1072
|
+
await fs.promises.unlink(`${this.options.dataFolder}/${storage.path}`);
|
|
1073
|
+
await this.removeConversationBlobVersion(internalId, conversation, options);
|
|
1074
|
+
await this.delete(internalId, options.user, options.app);
|
|
1075
|
+
await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
|
|
1076
|
+
}
|
|
1054
1077
|
async removeBlobViaPath(storage, options) {
|
|
1055
1078
|
if (!storage.length)
|
|
1056
1079
|
throw new Error('BLOB_NOT_FOUND');
|
|
@@ -1071,6 +1094,16 @@ export class StorageService extends Immutable {
|
|
|
1071
1094
|
}));
|
|
1072
1095
|
}
|
|
1073
1096
|
}
|
|
1097
|
+
async removeConversationBlobVersion(parentId, conversation, options) {
|
|
1098
|
+
const versions = await this.model.db.find({ parent: parentId, conversation }).toArray();
|
|
1099
|
+
if (versions.length) {
|
|
1100
|
+
await Promise.allSettled(versions.map(async (single) => {
|
|
1101
|
+
await fs.promises.unlink(`${this.options.dataFolder}/${single.path}`);
|
|
1102
|
+
await this.delete(single._id, options.user, options.app);
|
|
1103
|
+
await jetstreamService.publish(`edge.${single.app}.removed`, single);
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1074
1107
|
}
|
|
1075
1108
|
export default new StorageService({
|
|
1076
1109
|
dataFolder: process.env.MEDIA_FOLDERS_DATA,
|