@consilioweb/payload-support 4.0.0 → 6.0.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/README.md +272 -35
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +421 -73
- package/dist/index.d.cts +127 -38
- package/dist/index.d.ts +127 -38
- package/dist/index.js +415 -72
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/utils/db.d.ts +34 -0
- package/dist/utils/readSettings.d.ts +90 -0
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/BillingView/index.js +4 -4
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/ChatView/index.js +4 -4
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/CrmView/index.js +4 -4
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/EmailTrackingView/index.js +4 -4
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/ImportConversationView/index.js +4 -4
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/LogsView/index.js +4 -2
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/NewTicketView/index.js +4 -2
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/PendingEmailsView/index.js +4 -4
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/SupportDashboardView/index.js +4 -4
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketDetailView/index.js +4 -4
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketInboxView/index.js +4 -2
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TicketingSettingsView/index.js +4 -4
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/TimeDashboardView/index.js +4 -4
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/dist/views/shared/viewAccess.d.ts +29 -0
- package/dist/views/shared/viewAccess.js +24 -0
- package/package.json +31 -21
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/auth-2fa.ts +53 -8
- package/src/endpoints/capabilities.ts +2 -4
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/import-conversation.ts +2 -2
- package/src/endpoints/login.ts +13 -3
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/endpoints/push.ts +14 -1
- package/src/endpoints/statuses.ts +17 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/portal/login/page.tsx +14 -4
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/push.ts +22 -0
- package/src/utils/rateLimiter.ts +98 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/utils/twoFactorChallenge.ts +6 -1
- package/src/utils/urlSafety.ts +36 -1
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/BillingView/index.tsx +4 -4
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/ChatView/index.tsx +4 -4
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/CrmView/index.tsx +4 -4
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/EmailTrackingView/index.tsx +4 -4
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/ImportConversationView/index.tsx +4 -4
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/LogsView/index.tsx +4 -2
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/NewTicketView/index.tsx +4 -2
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/PendingEmailsView/index.tsx +4 -4
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/SupportDashboardView/index.tsx +4 -4
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketDetailView/index.tsx +4 -4
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketInboxView/index.tsx +4 -2
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TicketingSettingsView/index.tsx +4 -4
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/TimeDashboardView/index.tsx +4 -4
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/views/shared/viewAccess.ts +73 -0
- package/src/types/lucide-react.d.ts +0 -42
package/dist/index.js
CHANGED
|
@@ -72,15 +72,51 @@ var DEFAULT_SLUGS = {
|
|
|
72
72
|
users: "users",
|
|
73
73
|
media: "media"
|
|
74
74
|
};
|
|
75
|
+
var FIXED_SLUGS = {
|
|
76
|
+
clientSummaries: "client-summaries",
|
|
77
|
+
ticketCollaborators: "ticket-collaborators",
|
|
78
|
+
ticketFeedback: "ticket-feedback"
|
|
79
|
+
};
|
|
75
80
|
function resolveSlugs(overrides) {
|
|
76
81
|
return { ...DEFAULT_SLUGS, ...overrides };
|
|
77
82
|
}
|
|
83
|
+
var MAX_MEMORY_RATE_LIMIT_KEYS = 1e4;
|
|
78
84
|
var MemoryRateLimitStore = class {
|
|
85
|
+
constructor(maxKeys = MAX_MEMORY_RATE_LIMIT_KEYS) {
|
|
86
|
+
this.maxKeys = maxKeys;
|
|
87
|
+
}
|
|
88
|
+
maxKeys;
|
|
79
89
|
entries = /* @__PURE__ */ new Map();
|
|
90
|
+
/** Distinct keys currently held. Exposed so the ceiling can be asserted. */
|
|
91
|
+
get size() {
|
|
92
|
+
return this.entries.size;
|
|
93
|
+
}
|
|
94
|
+
/** Reclaims every window that has already closed. */
|
|
95
|
+
sweepExpired(now) {
|
|
96
|
+
for (const [key, entry] of this.entries) {
|
|
97
|
+
if (now > entry.resetAt) this.entries.delete(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Soonest-closing window first, so the ceiling drops the least useful entry. */
|
|
101
|
+
evictOldest() {
|
|
102
|
+
let oldestKey = null;
|
|
103
|
+
let oldestResetAt = Infinity;
|
|
104
|
+
for (const [key, entry] of this.entries) {
|
|
105
|
+
if (entry.resetAt < oldestResetAt) {
|
|
106
|
+
oldestResetAt = entry.resetAt;
|
|
107
|
+
oldestKey = key;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (oldestKey !== null) this.entries.delete(oldestKey);
|
|
111
|
+
}
|
|
80
112
|
async increment(key, windowMs) {
|
|
81
113
|
const now = Date.now();
|
|
82
114
|
const current = this.entries.get(key);
|
|
83
115
|
const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
|
|
116
|
+
if (!current && this.entries.size >= this.maxKeys) {
|
|
117
|
+
this.sweepExpired(now);
|
|
118
|
+
if (this.entries.size >= this.maxKeys) this.evictOldest();
|
|
119
|
+
}
|
|
84
120
|
this.entries.set(key, next);
|
|
85
121
|
return next;
|
|
86
122
|
}
|
|
@@ -164,6 +200,23 @@ function principalRateKey(user) {
|
|
|
164
200
|
const collection = typeof user.collection === "string" && user.collection ? user.collection : "unknown";
|
|
165
201
|
return `${collection}:${String(user.id)}`;
|
|
166
202
|
}
|
|
203
|
+
var MAX_IP_KEY_LENGTH = 45;
|
|
204
|
+
var IPV4_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
205
|
+
var IPV6_PATTERN = /^[0-9a-fA-F:.%]+$/;
|
|
206
|
+
function clientIpRateKey(req) {
|
|
207
|
+
const candidate = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim() || "";
|
|
208
|
+
return normalizeIpKey(candidate);
|
|
209
|
+
}
|
|
210
|
+
function normalizeIpKey(candidate) {
|
|
211
|
+
if (!candidate || candidate.length > MAX_IP_KEY_LENGTH) return "unknown";
|
|
212
|
+
const host = candidate.startsWith("[") && candidate.endsWith("]") ? candidate.slice(1, -1) : candidate;
|
|
213
|
+
if (!host) return "unknown";
|
|
214
|
+
if (IPV4_PATTERN.test(host)) {
|
|
215
|
+
return host.split(".").every((octet) => Number(octet) <= 255) ? host : "unknown";
|
|
216
|
+
}
|
|
217
|
+
if (host.includes(":") && IPV6_PATTERN.test(host)) return host.toLowerCase();
|
|
218
|
+
return "unknown";
|
|
219
|
+
}
|
|
167
220
|
var RateLimiter = class {
|
|
168
221
|
/**
|
|
169
222
|
* @param namespace Endpoint-scoped prefix for every key this limiter writes.
|
|
@@ -243,7 +296,7 @@ function createInboundEmailEndpoint(capability, store) {
|
|
|
243
296
|
if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
|
|
244
297
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
245
298
|
}
|
|
246
|
-
const ip =
|
|
299
|
+
const ip = clientIpRateKey(req);
|
|
247
300
|
if (await limiter.check(ip, req)) {
|
|
248
301
|
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
249
302
|
}
|
|
@@ -2800,6 +2853,10 @@ function createStatusesEndpoint(slugs) {
|
|
|
2800
2853
|
if (!req.user) {
|
|
2801
2854
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2802
2855
|
}
|
|
2856
|
+
const collection = req.user.collection;
|
|
2857
|
+
if (collection !== slugs.users && collection !== slugs.supportClients) {
|
|
2858
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
2859
|
+
}
|
|
2803
2860
|
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2804
2861
|
sort: "sortOrder",
|
|
2805
2862
|
limit: 100,
|
|
@@ -2936,6 +2993,51 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2936
2993
|
};
|
|
2937
2994
|
}
|
|
2938
2995
|
|
|
2996
|
+
// src/utils/retention.ts
|
|
2997
|
+
var DEFAULT_RETENTION = {
|
|
2998
|
+
authLogsDays: 180,
|
|
2999
|
+
emailLogsDays: 365,
|
|
3000
|
+
cron: "0 30 3 * * *",
|
|
3001
|
+
queue: "default"
|
|
3002
|
+
};
|
|
3003
|
+
var PURGE_LOGS_TASK_SLUG = "support-purge-logs";
|
|
3004
|
+
function purgeableCollections(slugs) {
|
|
3005
|
+
return {
|
|
3006
|
+
"email-logs": slugs.emailLogs,
|
|
3007
|
+
"auth-logs": slugs.authLogs
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
async function purgeOlderThan(payload, slug, days, req) {
|
|
3011
|
+
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
3012
|
+
const result = await dbDelete(payload, slug, {
|
|
3013
|
+
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
3014
|
+
overrideAccess: true,
|
|
3015
|
+
...req ? { req } : {}
|
|
3016
|
+
});
|
|
3017
|
+
return Array.isArray(result?.docs) ? result.docs.length : 0;
|
|
3018
|
+
}
|
|
3019
|
+
async function runScheduledPurge(payload, slugs, retention, req) {
|
|
3020
|
+
if (retention === false) return { purged: {}, skipped: ["auth-logs", "email-logs"] };
|
|
3021
|
+
const plan = [
|
|
3022
|
+
["auth-logs", slugs.authLogs, retention?.authLogsDays ?? DEFAULT_RETENTION.authLogsDays],
|
|
3023
|
+
["email-logs", slugs.emailLogs, retention?.emailLogsDays ?? DEFAULT_RETENTION.emailLogsDays]
|
|
3024
|
+
];
|
|
3025
|
+
const purged = {};
|
|
3026
|
+
const skipped = [];
|
|
3027
|
+
for (const [name, slug, days] of plan) {
|
|
3028
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
3029
|
+
skipped.push(name);
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
if (!payload.collections?.[slug]) {
|
|
3033
|
+
skipped.push(name);
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
purged[name] = await purgeOlderThan(payload, slug, days, req);
|
|
3037
|
+
}
|
|
3038
|
+
return { purged, skipped };
|
|
3039
|
+
}
|
|
3040
|
+
|
|
2939
3041
|
// src/endpoints/purge-logs.ts
|
|
2940
3042
|
function createPurgeLogsEndpoint(slugs) {
|
|
2941
3043
|
return {
|
|
@@ -2948,19 +3050,11 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2948
3050
|
const url = new URL(req.url);
|
|
2949
3051
|
const collection = url.searchParams.get("collection");
|
|
2950
3052
|
const days = Number(url.searchParams.get("days") || "0");
|
|
2951
|
-
const allowedCollections =
|
|
2952
|
-
"email-logs": slugs.emailLogs,
|
|
2953
|
-
"auth-logs": slugs.authLogs
|
|
2954
|
-
};
|
|
3053
|
+
const allowedCollections = purgeableCollections(slugs);
|
|
2955
3054
|
if (!collection || !allowedCollections[collection]) {
|
|
2956
3055
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2957
3056
|
}
|
|
2958
|
-
const
|
|
2959
|
-
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2960
|
-
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2961
|
-
overrideAccess: true
|
|
2962
|
-
});
|
|
2963
|
-
const count = Array.isArray(result.docs) ? result.docs.length : 0;
|
|
3057
|
+
const count = await purgeOlderThan(payload, allowedCollections[collection], days, req);
|
|
2964
3058
|
return Response.json({
|
|
2965
3059
|
purged: count,
|
|
2966
3060
|
collection,
|
|
@@ -2990,7 +3084,7 @@ function createChatbotEndpoint(slugs, store, maxPerHour) {
|
|
|
2990
3084
|
method: "post",
|
|
2991
3085
|
handler: async (req) => {
|
|
2992
3086
|
try {
|
|
2993
|
-
const ip =
|
|
3087
|
+
const ip = clientIpRateKey(req);
|
|
2994
3088
|
if (await chatbotLimiter.check(ip, req)) {
|
|
2995
3089
|
return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
|
|
2996
3090
|
}
|
|
@@ -4683,6 +4777,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
4683
4777
|
}
|
|
4684
4778
|
|
|
4685
4779
|
// src/endpoints/export-data.ts
|
|
4780
|
+
var EMPTY = { docs: [] };
|
|
4686
4781
|
function createExportDataEndpoint(slugs) {
|
|
4687
4782
|
return {
|
|
4688
4783
|
path: "/support/export-data",
|
|
@@ -4691,7 +4786,16 @@ function createExportDataEndpoint(slugs) {
|
|
|
4691
4786
|
try {
|
|
4692
4787
|
const payload = req.payload;
|
|
4693
4788
|
requireClient(req, slugs);
|
|
4694
|
-
const
|
|
4789
|
+
const isRegistered = (slug) => Boolean(payload.collections?.[slug]);
|
|
4790
|
+
const [
|
|
4791
|
+
clientData,
|
|
4792
|
+
ticketsResult,
|
|
4793
|
+
messagesResult,
|
|
4794
|
+
surveysResult,
|
|
4795
|
+
chatResult,
|
|
4796
|
+
feedbackResult,
|
|
4797
|
+
summariesResult
|
|
4798
|
+
] = await Promise.all([
|
|
4695
4799
|
dbFindByID(payload, slugs.supportClients, {
|
|
4696
4800
|
id: req.user.id,
|
|
4697
4801
|
depth: 0,
|
|
@@ -4717,12 +4821,35 @@ function createExportDataEndpoint(slugs) {
|
|
|
4717
4821
|
limit: 500,
|
|
4718
4822
|
depth: 0,
|
|
4719
4823
|
overrideAccess: true
|
|
4720
|
-
})
|
|
4824
|
+
}),
|
|
4825
|
+
isRegistered(slugs.chatMessages) ? dbFind(payload, slugs.chatMessages, {
|
|
4826
|
+
where: { client: { equals: req.user.id } },
|
|
4827
|
+
limit: 5e3,
|
|
4828
|
+
depth: 0,
|
|
4829
|
+
overrideAccess: true
|
|
4830
|
+
}) : EMPTY,
|
|
4831
|
+
isRegistered(FIXED_SLUGS.ticketFeedback) ? dbFind(payload, FIXED_SLUGS.ticketFeedback, {
|
|
4832
|
+
where: { client: { equals: req.user.id } },
|
|
4833
|
+
limit: 500,
|
|
4834
|
+
depth: 0,
|
|
4835
|
+
overrideAccess: true
|
|
4836
|
+
}) : EMPTY,
|
|
4837
|
+
isRegistered(FIXED_SLUGS.clientSummaries) ? dbFind(payload, FIXED_SLUGS.clientSummaries, {
|
|
4838
|
+
where: { client: { equals: req.user.id } },
|
|
4839
|
+
limit: 100,
|
|
4840
|
+
depth: 0,
|
|
4841
|
+
overrideAccess: true
|
|
4842
|
+
}) : EMPTY
|
|
4721
4843
|
]);
|
|
4844
|
+
const ticketIds = ticketsResult.docs.map((t) => t.id);
|
|
4845
|
+
const timeResult = ticketIds.length > 0 && isRegistered(slugs.timeEntries) ? await dbFind(payload, slugs.timeEntries, {
|
|
4846
|
+
where: { ticket: { in: ticketIds } },
|
|
4847
|
+
limit: 5e3,
|
|
4848
|
+
depth: 0,
|
|
4849
|
+
overrideAccess: true
|
|
4850
|
+
}) : EMPTY;
|
|
4722
4851
|
const c = clientData;
|
|
4723
|
-
const
|
|
4724
|
-
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4725
|
-
exportType: "RGPD - Export des donn\xE9es personnelles",
|
|
4852
|
+
const providedByYou = {
|
|
4726
4853
|
profile: {
|
|
4727
4854
|
email: c.email,
|
|
4728
4855
|
firstName: c.firstName,
|
|
@@ -4751,8 +4878,50 @@ function createExportDataEndpoint(slugs) {
|
|
|
4751
4878
|
rating: s.rating,
|
|
4752
4879
|
comment: s.comment,
|
|
4753
4880
|
createdAt: s.createdAt
|
|
4881
|
+
})),
|
|
4882
|
+
feedback: feedbackResult.docs.map((f) => ({
|
|
4883
|
+
ticketId: f.ticket,
|
|
4884
|
+
rating: f.rating,
|
|
4885
|
+
comment: f.comment,
|
|
4886
|
+
submittedFrom: f.submittedFrom,
|
|
4887
|
+
createdAt: f.createdAt
|
|
4888
|
+
})),
|
|
4889
|
+
chatMessages: chatResult.docs.map((m) => ({
|
|
4890
|
+
session: m.session,
|
|
4891
|
+
senderType: m.senderType,
|
|
4892
|
+
message: m.message,
|
|
4893
|
+
createdAt: m.createdAt
|
|
4894
|
+
})),
|
|
4895
|
+
timeEntries: timeResult.docs.map((e) => ({
|
|
4896
|
+
ticketId: e.ticket,
|
|
4897
|
+
duration: e.duration,
|
|
4898
|
+
date: e.date,
|
|
4899
|
+
description: e.description,
|
|
4900
|
+
billable: e.billable
|
|
4754
4901
|
}))
|
|
4755
4902
|
};
|
|
4903
|
+
const exportData = {
|
|
4904
|
+
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4905
|
+
exportType: "RGPD - Export des donn\xE9es personnelles",
|
|
4906
|
+
providedByYou: {
|
|
4907
|
+
legalBasis: "Art. 20 RGPD - droit \xE0 la portabilit\xE9",
|
|
4908
|
+
...providedByYou
|
|
4909
|
+
},
|
|
4910
|
+
derivedData: {
|
|
4911
|
+
legalBasis: "Art. 15 RGPD - droit d'acc\xE8s (donn\xE9es d\xE9riv\xE9es, hors portabilit\xE9)",
|
|
4912
|
+
clientSummaries: summariesResult.docs.map((s) => ({
|
|
4913
|
+
summary: s.summary,
|
|
4914
|
+
recurringTopics: s.recurringTopics,
|
|
4915
|
+
patterns: s.patterns,
|
|
4916
|
+
keyFacts: s.keyFacts,
|
|
4917
|
+
ticketCount: s.ticketCount,
|
|
4918
|
+
messageCount: s.messageCount,
|
|
4919
|
+
averageSatisfaction: s.averageSatisfaction,
|
|
4920
|
+
generatedAt: s.generatedAt,
|
|
4921
|
+
aiModel: s.aiModel
|
|
4922
|
+
}))
|
|
4923
|
+
}
|
|
4924
|
+
};
|
|
4756
4925
|
const json = JSON.stringify(exportData, null, 2);
|
|
4757
4926
|
return new Response(json, {
|
|
4758
4927
|
status: 200,
|
|
@@ -5196,13 +5365,14 @@ function verifyTwoFactorChallenge(email, token, now = Date.now()) {
|
|
|
5196
5365
|
}
|
|
5197
5366
|
|
|
5198
5367
|
// src/endpoints/login.ts
|
|
5368
|
+
var MAX_LOGGED_USER_AGENT = 256;
|
|
5199
5369
|
function createLoginEndpoint(slugs, store) {
|
|
5200
5370
|
const loginLimiter = new RateLimiter(15 * 6e4, 10, store, "login");
|
|
5201
5371
|
return {
|
|
5202
5372
|
path: "/support/login",
|
|
5203
5373
|
method: "post",
|
|
5204
5374
|
handler: async (req) => {
|
|
5205
|
-
const ip =
|
|
5375
|
+
const ip = clientIpRateKey(req);
|
|
5206
5376
|
if (await loginLimiter.check(ip, req)) {
|
|
5207
5377
|
return Response.json(
|
|
5208
5378
|
{ error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
|
|
@@ -5217,7 +5387,7 @@ function createLoginEndpoint(slugs, store) {
|
|
|
5217
5387
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5218
5388
|
}
|
|
5219
5389
|
const { email, password } = body;
|
|
5220
|
-
const userAgent = req.headers.get("user-agent") || "";
|
|
5390
|
+
const userAgent = (req.headers.get("user-agent") || "").slice(0, MAX_LOGGED_USER_AGENT);
|
|
5221
5391
|
if (!email || !password) {
|
|
5222
5392
|
return Response.json({ error: "Email et mot de passe requis." }, { status: 400 });
|
|
5223
5393
|
}
|
|
@@ -5305,7 +5475,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5305
5475
|
if (!action || !email) {
|
|
5306
5476
|
return Response.json({ error: "Param\xE8tres manquants" }, { status: 400 });
|
|
5307
5477
|
}
|
|
5308
|
-
const
|
|
5478
|
+
const limiterKey = normalizeEmail(email);
|
|
5479
|
+
const sendResponse = () => {
|
|
5480
|
+
let refreshed;
|
|
5481
|
+
try {
|
|
5482
|
+
refreshed = issueTwoFactorChallenge(email);
|
|
5483
|
+
} catch {
|
|
5484
|
+
}
|
|
5485
|
+
return Response.json({
|
|
5486
|
+
success: true,
|
|
5487
|
+
message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9.",
|
|
5488
|
+
...refreshed ? { challenge: refreshed } : {}
|
|
5489
|
+
});
|
|
5490
|
+
};
|
|
5309
5491
|
if (action === "send") {
|
|
5310
5492
|
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5311
5493
|
return Response.json(
|
|
@@ -5313,8 +5495,8 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5313
5495
|
{ status: 401 }
|
|
5314
5496
|
);
|
|
5315
5497
|
}
|
|
5316
|
-
if (await sendLimiter.check(
|
|
5317
|
-
return
|
|
5498
|
+
if (await sendLimiter.check(limiterKey, req)) {
|
|
5499
|
+
return sendResponse();
|
|
5318
5500
|
}
|
|
5319
5501
|
const clients = await dbFind(payload, slugs.supportClients, {
|
|
5320
5502
|
where: { email: { equals: email } },
|
|
@@ -5323,7 +5505,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5323
5505
|
overrideAccess: true
|
|
5324
5506
|
});
|
|
5325
5507
|
if (clients.docs.length === 0) {
|
|
5326
|
-
return
|
|
5508
|
+
return sendResponse();
|
|
5327
5509
|
}
|
|
5328
5510
|
const client = clients.docs[0];
|
|
5329
5511
|
const plainCode = generateSecureCode();
|
|
@@ -5348,13 +5530,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5348
5530
|
<p style="font-size: 13px; color: #6b7280;">Ce code est valable 10 minutes.</p>
|
|
5349
5531
|
</div>`
|
|
5350
5532
|
});
|
|
5351
|
-
return
|
|
5533
|
+
return sendResponse();
|
|
5352
5534
|
}
|
|
5353
5535
|
if (action === "verify") {
|
|
5536
|
+
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5537
|
+
return Response.json(
|
|
5538
|
+
{ error: "Authentification requise avant la v\xE9rification d'un code." },
|
|
5539
|
+
{ status: 401 }
|
|
5540
|
+
);
|
|
5541
|
+
}
|
|
5354
5542
|
if (!code) {
|
|
5355
5543
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
5356
5544
|
}
|
|
5357
|
-
if (await verifyLimiter.check(
|
|
5545
|
+
if (await verifyLimiter.check(limiterKey, req)) {
|
|
5358
5546
|
return Response.json(
|
|
5359
5547
|
{ error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
|
|
5360
5548
|
{ status: 429 }
|
|
@@ -5395,7 +5583,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5395
5583
|
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
5396
5584
|
overrideAccess: true
|
|
5397
5585
|
});
|
|
5398
|
-
verifyLimiter.reset(
|
|
5586
|
+
await verifyLimiter.reset(limiterKey, req);
|
|
5399
5587
|
return Response.json({ success: true, verified: true });
|
|
5400
5588
|
}
|
|
5401
5589
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -5647,8 +5835,15 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
5647
5835
|
}
|
|
5648
5836
|
};
|
|
5649
5837
|
}
|
|
5650
|
-
|
|
5651
|
-
|
|
5838
|
+
function collectAttachmentIds(docs, into) {
|
|
5839
|
+
for (const doc of docs) {
|
|
5840
|
+
for (const attachment of doc.attachments ?? []) {
|
|
5841
|
+
const file = attachment?.file;
|
|
5842
|
+
const id = file != null && typeof file === "object" ? file.id : file;
|
|
5843
|
+
if (id != null) into.add(id);
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5652
5847
|
function createDeleteAccountEndpoint(slugs) {
|
|
5653
5848
|
return {
|
|
5654
5849
|
path: "/support/delete-account",
|
|
@@ -5677,44 +5872,135 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
5677
5872
|
);
|
|
5678
5873
|
}
|
|
5679
5874
|
const clientId = req.user.id;
|
|
5680
|
-
const
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
select: { id: true }
|
|
5686
|
-
});
|
|
5687
|
-
const ticketIds = tickets.docs.map((t) => t.id);
|
|
5688
|
-
if (ticketIds.length > 0) {
|
|
5689
|
-
await dbDelete(payload, slugs.ticketMessages, {
|
|
5690
|
-
where: { ticket: { in: ticketIds } },
|
|
5691
|
-
overrideAccess: true
|
|
5692
|
-
});
|
|
5693
|
-
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
5694
|
-
where: { ticket: { in: ticketIds } },
|
|
5695
|
-
overrideAccess: true
|
|
5696
|
-
});
|
|
5697
|
-
await dbDelete(payload, slugs.timeEntries, {
|
|
5698
|
-
where: { ticket: { in: ticketIds } },
|
|
5699
|
-
overrideAccess: true
|
|
5700
|
-
});
|
|
5701
|
-
await dbDelete(payload, slugs.tickets, {
|
|
5875
|
+
const clientEmail = req.user.email;
|
|
5876
|
+
const isRegistered = (slug) => Boolean(payload.collections?.[slug]);
|
|
5877
|
+
const startedTransaction = await initTransaction(req);
|
|
5878
|
+
try {
|
|
5879
|
+
const tickets = await dbFind(payload, slugs.tickets, {
|
|
5702
5880
|
where: { client: { equals: clientId } },
|
|
5703
|
-
|
|
5881
|
+
limit: 1e4,
|
|
5882
|
+
depth: 0,
|
|
5883
|
+
overrideAccess: true,
|
|
5884
|
+
select: { id: true },
|
|
5885
|
+
req
|
|
5886
|
+
});
|
|
5887
|
+
const ticketIds = tickets.docs.map((t) => t.id);
|
|
5888
|
+
const attachmentIds = /* @__PURE__ */ new Set();
|
|
5889
|
+
if (ticketIds.length > 0) {
|
|
5890
|
+
const messages = await dbFind(
|
|
5891
|
+
payload,
|
|
5892
|
+
slugs.ticketMessages,
|
|
5893
|
+
{
|
|
5894
|
+
where: { ticket: { in: ticketIds } },
|
|
5895
|
+
limit: 1e4,
|
|
5896
|
+
depth: 0,
|
|
5897
|
+
overrideAccess: true,
|
|
5898
|
+
req
|
|
5899
|
+
}
|
|
5900
|
+
);
|
|
5901
|
+
collectAttachmentIds(messages.docs, attachmentIds);
|
|
5902
|
+
}
|
|
5903
|
+
if (isRegistered(slugs.pendingEmails)) {
|
|
5904
|
+
const pendingEmails = await dbFind(
|
|
5905
|
+
payload,
|
|
5906
|
+
slugs.pendingEmails,
|
|
5907
|
+
{
|
|
5908
|
+
where: { client: { equals: clientId } },
|
|
5909
|
+
limit: 1e4,
|
|
5910
|
+
depth: 0,
|
|
5911
|
+
overrideAccess: true,
|
|
5912
|
+
req
|
|
5913
|
+
}
|
|
5914
|
+
);
|
|
5915
|
+
collectAttachmentIds(pendingEmails.docs, attachmentIds);
|
|
5916
|
+
}
|
|
5917
|
+
if (ticketIds.length > 0) {
|
|
5918
|
+
for (const slug of [FIXED_SLUGS.ticketFeedback, FIXED_SLUGS.ticketCollaborators]) {
|
|
5919
|
+
if (!isRegistered(slug)) continue;
|
|
5920
|
+
await dbDelete(payload, slug, {
|
|
5921
|
+
where: {
|
|
5922
|
+
or: [
|
|
5923
|
+
{ ticket: { in: ticketIds } },
|
|
5924
|
+
{ client: { equals: clientId } }
|
|
5925
|
+
]
|
|
5926
|
+
},
|
|
5927
|
+
overrideAccess: true,
|
|
5928
|
+
req
|
|
5929
|
+
});
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
if (ticketIds.length > 0) {
|
|
5933
|
+
await dbDelete(payload, slugs.tickets, {
|
|
5934
|
+
where: { client: { equals: clientId } },
|
|
5935
|
+
overrideAccess: true,
|
|
5936
|
+
req
|
|
5937
|
+
});
|
|
5938
|
+
}
|
|
5939
|
+
for (const slug of [
|
|
5940
|
+
slugs.satisfactionSurveys,
|
|
5941
|
+
slugs.chatMessages,
|
|
5942
|
+
slugs.notificationQueue,
|
|
5943
|
+
slugs.pendingEmails,
|
|
5944
|
+
FIXED_SLUGS.clientSummaries,
|
|
5945
|
+
FIXED_SLUGS.ticketFeedback
|
|
5946
|
+
]) {
|
|
5947
|
+
if (!isRegistered(slug)) continue;
|
|
5948
|
+
await dbDelete(payload, slug, {
|
|
5949
|
+
where: { client: { equals: clientId } },
|
|
5950
|
+
overrideAccess: true,
|
|
5951
|
+
req
|
|
5952
|
+
});
|
|
5953
|
+
}
|
|
5954
|
+
if (isRegistered(FIXED_SLUGS.ticketCollaborators)) {
|
|
5955
|
+
await dbDelete(payload, FIXED_SLUGS.ticketCollaborators, {
|
|
5956
|
+
where: {
|
|
5957
|
+
or: [
|
|
5958
|
+
{ client: { equals: clientId } },
|
|
5959
|
+
...clientEmail ? [{ email: { equals: clientEmail } }] : []
|
|
5960
|
+
]
|
|
5961
|
+
},
|
|
5962
|
+
overrideAccess: true,
|
|
5963
|
+
req
|
|
5964
|
+
});
|
|
5965
|
+
}
|
|
5966
|
+
if (clientEmail) {
|
|
5967
|
+
if (isRegistered(slugs.authLogs)) {
|
|
5968
|
+
await dbDelete(payload, slugs.authLogs, {
|
|
5969
|
+
where: { email: { equals: clientEmail } },
|
|
5970
|
+
overrideAccess: true,
|
|
5971
|
+
req
|
|
5972
|
+
});
|
|
5973
|
+
}
|
|
5974
|
+
if (isRegistered(slugs.emailLogs)) {
|
|
5975
|
+
await dbDelete(payload, slugs.emailLogs, {
|
|
5976
|
+
where: {
|
|
5977
|
+
or: [
|
|
5978
|
+
{ senderEmail: { equals: clientEmail } },
|
|
5979
|
+
{ recipientEmail: { equals: clientEmail } }
|
|
5980
|
+
]
|
|
5981
|
+
},
|
|
5982
|
+
overrideAccess: true,
|
|
5983
|
+
req
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
for (const id of isRegistered(slugs.media) ? attachmentIds : []) {
|
|
5988
|
+
await dbDelete(payload, slugs.media, {
|
|
5989
|
+
id,
|
|
5990
|
+
overrideAccess: true,
|
|
5991
|
+
req
|
|
5992
|
+
});
|
|
5993
|
+
}
|
|
5994
|
+
await dbDelete(payload, slugs.supportClients, {
|
|
5995
|
+
id: clientId,
|
|
5996
|
+
overrideAccess: true,
|
|
5997
|
+
req
|
|
5704
5998
|
});
|
|
5999
|
+
if (startedTransaction) await commitTransaction(req);
|
|
6000
|
+
} catch (err) {
|
|
6001
|
+
if (startedTransaction) await killTransaction(req);
|
|
6002
|
+
throw err;
|
|
5705
6003
|
}
|
|
5706
|
-
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
5707
|
-
where: { client: { equals: clientId } },
|
|
5708
|
-
overrideAccess: true
|
|
5709
|
-
});
|
|
5710
|
-
await dbDelete(payload, slugs.chatMessages, {
|
|
5711
|
-
where: { client: { equals: clientId } },
|
|
5712
|
-
overrideAccess: true
|
|
5713
|
-
});
|
|
5714
|
-
await dbDelete(payload, slugs.supportClients, {
|
|
5715
|
-
id: clientId,
|
|
5716
|
-
overrideAccess: true
|
|
5717
|
-
});
|
|
5718
6004
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5719
6005
|
const secure = process.env.NODE_ENV === "production";
|
|
5720
6006
|
headers.append(
|
|
@@ -5957,7 +6243,7 @@ function createImportConversationEndpoint(slugs, store) {
|
|
|
5957
6243
|
if (!isAuthed) {
|
|
5958
6244
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5959
6245
|
}
|
|
5960
|
-
const ip = req
|
|
6246
|
+
const ip = clientIpRateKey(req);
|
|
5961
6247
|
if (await importLimiter.check(ip, req)) {
|
|
5962
6248
|
return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
|
|
5963
6249
|
}
|
|
@@ -6165,6 +6451,21 @@ var WEBHOOK_URL_MESSAGES = {
|
|
|
6165
6451
|
scheme_not_allowed: "Seules les URL https:// sont accept\xE9es.",
|
|
6166
6452
|
private_host: "Les adresses priv\xE9es, loopback et link-local sont interdites (SSRF)."
|
|
6167
6453
|
};
|
|
6454
|
+
var MAX_PUSH_ENDPOINT_LENGTH = 2048;
|
|
6455
|
+
function validatePushEndpoint(raw) {
|
|
6456
|
+
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_PUSH_ENDPOINT_LENGTH) {
|
|
6457
|
+
return { ok: false, reason: "invalid_url" };
|
|
6458
|
+
}
|
|
6459
|
+
let url;
|
|
6460
|
+
try {
|
|
6461
|
+
url = new URL(raw.trim());
|
|
6462
|
+
} catch {
|
|
6463
|
+
return { ok: false, reason: "invalid_url" };
|
|
6464
|
+
}
|
|
6465
|
+
if (url.protocol !== "https:") return { ok: false, reason: "scheme_not_allowed" };
|
|
6466
|
+
if (isBlockedHost(url.hostname)) return { ok: false, reason: "private_host" };
|
|
6467
|
+
return { ok: true, url };
|
|
6468
|
+
}
|
|
6168
6469
|
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
6169
6470
|
var LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("lookup-timed-out");
|
|
6170
6471
|
var NONEXISTENT_HOST_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA", "NOTFOUND"]);
|
|
@@ -6620,6 +6921,15 @@ async function sendPushToUser(payload, slugs, userId, notification) {
|
|
|
6620
6921
|
for (const s of subs.docs) {
|
|
6621
6922
|
const row = s;
|
|
6622
6923
|
if (!row.endpoint || !row.p256dh || !row.auth) continue;
|
|
6924
|
+
const check = validatePushEndpoint(row.endpoint);
|
|
6925
|
+
if (!check.ok || !check.url) {
|
|
6926
|
+
console.warn("[support] Push endpoint refused (unsafe URL):", check.reason);
|
|
6927
|
+
continue;
|
|
6928
|
+
}
|
|
6929
|
+
if (!await assertPublicHost(check.url.hostname)) {
|
|
6930
|
+
console.warn("[support] Push endpoint refused (host resolves to a private address)");
|
|
6931
|
+
continue;
|
|
6932
|
+
}
|
|
6623
6933
|
try {
|
|
6624
6934
|
await webpush.sendNotification(
|
|
6625
6935
|
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
|
@@ -6670,7 +6980,14 @@ function createPushSubscribeEndpoint(slugs) {
|
|
|
6670
6980
|
if (!endpoint || !p256dh || !auth) {
|
|
6671
6981
|
return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
|
|
6672
6982
|
}
|
|
6673
|
-
const
|
|
6983
|
+
const endpointCheck = validatePushEndpoint(endpoint);
|
|
6984
|
+
if (!endpointCheck.ok) {
|
|
6985
|
+
return Response.json(
|
|
6986
|
+
{ error: WEBHOOK_URL_MESSAGES[endpointCheck.reason || "invalid_url"] },
|
|
6987
|
+
{ status: 400 }
|
|
6988
|
+
);
|
|
6989
|
+
}
|
|
6990
|
+
const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: (req.headers.get("user-agent") || "").slice(0, 256) };
|
|
6674
6991
|
const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
|
|
6675
6992
|
if (existing.docs.length > 0) {
|
|
6676
6993
|
await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
|
|
@@ -8326,9 +8643,11 @@ function createCascadeDelete(slugs) {
|
|
|
8326
8643
|
return async ({ id, req }) => {
|
|
8327
8644
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
8328
8645
|
for (const slug of collections) {
|
|
8646
|
+
if (!req.payload.collections?.[slug]) continue;
|
|
8329
8647
|
await dbDelete(req.payload, slug, {
|
|
8330
8648
|
where: { ticket: { equals: id } },
|
|
8331
|
-
overrideAccess: true
|
|
8649
|
+
overrideAccess: true,
|
|
8650
|
+
req
|
|
8332
8651
|
});
|
|
8333
8652
|
}
|
|
8334
8653
|
};
|
|
@@ -9816,7 +10135,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9816
10135
|
}
|
|
9817
10136
|
|
|
9818
10137
|
// src/collections/TimeEntries.ts
|
|
9819
|
-
async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
10138
|
+
async function recalculateTicketTime(payload, slugs, ticketId, req) {
|
|
9820
10139
|
let totalMinutes = 0;
|
|
9821
10140
|
let page = 1;
|
|
9822
10141
|
let hasMore = true;
|
|
@@ -9828,7 +10147,8 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
|
9828
10147
|
page,
|
|
9829
10148
|
depth: 0,
|
|
9830
10149
|
overrideAccess: true,
|
|
9831
|
-
select: { duration: true }
|
|
10150
|
+
select: { duration: true },
|
|
10151
|
+
req
|
|
9832
10152
|
});
|
|
9833
10153
|
for (const entry of entries.docs) {
|
|
9834
10154
|
totalMinutes += entry.duration || 0;
|
|
@@ -9840,14 +10160,15 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
|
9840
10160
|
collection: slugs.tickets,
|
|
9841
10161
|
id: ticketId,
|
|
9842
10162
|
data: { totalTimeMinutes: totalMinutes },
|
|
9843
|
-
overrideAccess: true
|
|
10163
|
+
overrideAccess: true,
|
|
10164
|
+
req
|
|
9844
10165
|
});
|
|
9845
10166
|
}
|
|
9846
10167
|
function createRecalculateTicketTime(slugs) {
|
|
9847
10168
|
return async ({ doc, req }) => {
|
|
9848
10169
|
if (!doc.ticket) return;
|
|
9849
10170
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9850
|
-
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
10171
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req);
|
|
9851
10172
|
};
|
|
9852
10173
|
}
|
|
9853
10174
|
function createRecalculateTicketTimeOnDelete(slugs) {
|
|
@@ -9855,7 +10176,7 @@ function createRecalculateTicketTimeOnDelete(slugs) {
|
|
|
9855
10176
|
if (!doc?.ticket) return;
|
|
9856
10177
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9857
10178
|
try {
|
|
9858
|
-
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
10179
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req);
|
|
9859
10180
|
} catch (err) {
|
|
9860
10181
|
console.error("[support] Failed to recalculate ticket time after delete:", err);
|
|
9861
10182
|
}
|
|
@@ -11726,8 +12047,30 @@ function supportPlugin(config) {
|
|
|
11726
12047
|
rateLimitStore,
|
|
11727
12048
|
capabilities: config?.capabilities
|
|
11728
12049
|
});
|
|
12050
|
+
const retention = config?.retention === false ? void 0 : config?.retention;
|
|
12051
|
+
const retentionEnabled = Boolean(retention);
|
|
12052
|
+
const purgeTask = {
|
|
12053
|
+
slug: PURGE_LOGS_TASK_SLUG,
|
|
12054
|
+
label: "Support \u2014 purge des journaux",
|
|
12055
|
+
schedule: [
|
|
12056
|
+
{
|
|
12057
|
+
cron: retention?.cron ?? DEFAULT_RETENTION.cron,
|
|
12058
|
+
queue: retention?.queue ?? DEFAULT_RETENTION.queue
|
|
12059
|
+
}
|
|
12060
|
+
],
|
|
12061
|
+
handler: async ({ req }) => {
|
|
12062
|
+
const result = await runScheduledPurge(req.payload, slugs, retention, req);
|
|
12063
|
+
return { output: result };
|
|
12064
|
+
}
|
|
12065
|
+
};
|
|
12066
|
+
const existingJobs = incomingConfig.jobs;
|
|
12067
|
+
const jobs = retentionEnabled ? {
|
|
12068
|
+
...existingJobs,
|
|
12069
|
+
tasks: [...existingJobs?.tasks ?? [], purgeTask]
|
|
12070
|
+
} : existingJobs;
|
|
11729
12071
|
return {
|
|
11730
12072
|
...incomingConfig,
|
|
12073
|
+
jobs,
|
|
11731
12074
|
// Publish the resolved staff collection so the server-side readers share
|
|
11732
12075
|
// ONE source of truth with the writers. `requireAdmin` compares against
|
|
11733
12076
|
// `slugs.users`; the `payload-preferences` reads used to scope themselves
|
|
@@ -11752,4 +12095,4 @@ function supportPlugin(config) {
|
|
|
11752
12095
|
};
|
|
11753
12096
|
}
|
|
11754
12097
|
|
|
11755
|
-
export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_TICKETING_FEATURES, DEFAULT_USER_PREFS, MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createLoginEndpoint, createMacrosCollection, createOAuthGoogleEndpoint, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createTrackOpenEndpoint, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, normalizeFeatures, projectAutoClose, readSupportSettings, readSupportSettingsState, readUserPrefs, resolveSlugs, stripProjectedFeatures, supportPlugin, validateInboundEmailPayload, verifySecret };
|
|
12098
|
+
export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_RETENTION, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_TICKETING_FEATURES, DEFAULT_USER_PREFS, MemoryRateLimitStore, PURGE_LOGS_TASK_SLUG, PayloadRateLimitStore, RateLimiter, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createLoginEndpoint, createMacrosCollection, createOAuthGoogleEndpoint, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createTrackOpenEndpoint, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, normalizeFeatures, projectAutoClose, purgeOlderThan, purgeableCollections, readSupportSettings, readSupportSettingsState, readUserPrefs, resolveSlugs, runScheduledPurge, stripProjectedFeatures, supportPlugin, validateInboundEmailPayload, verifySecret };
|