@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.cjs
CHANGED
|
@@ -81,15 +81,51 @@ var DEFAULT_SLUGS = {
|
|
|
81
81
|
users: "users",
|
|
82
82
|
media: "media"
|
|
83
83
|
};
|
|
84
|
+
var FIXED_SLUGS = {
|
|
85
|
+
clientSummaries: "client-summaries",
|
|
86
|
+
ticketCollaborators: "ticket-collaborators",
|
|
87
|
+
ticketFeedback: "ticket-feedback"
|
|
88
|
+
};
|
|
84
89
|
function resolveSlugs(overrides) {
|
|
85
90
|
return { ...DEFAULT_SLUGS, ...overrides };
|
|
86
91
|
}
|
|
92
|
+
var MAX_MEMORY_RATE_LIMIT_KEYS = 1e4;
|
|
87
93
|
var MemoryRateLimitStore = class {
|
|
94
|
+
constructor(maxKeys = MAX_MEMORY_RATE_LIMIT_KEYS) {
|
|
95
|
+
this.maxKeys = maxKeys;
|
|
96
|
+
}
|
|
97
|
+
maxKeys;
|
|
88
98
|
entries = /* @__PURE__ */ new Map();
|
|
99
|
+
/** Distinct keys currently held. Exposed so the ceiling can be asserted. */
|
|
100
|
+
get size() {
|
|
101
|
+
return this.entries.size;
|
|
102
|
+
}
|
|
103
|
+
/** Reclaims every window that has already closed. */
|
|
104
|
+
sweepExpired(now) {
|
|
105
|
+
for (const [key, entry] of this.entries) {
|
|
106
|
+
if (now > entry.resetAt) this.entries.delete(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Soonest-closing window first, so the ceiling drops the least useful entry. */
|
|
110
|
+
evictOldest() {
|
|
111
|
+
let oldestKey = null;
|
|
112
|
+
let oldestResetAt = Infinity;
|
|
113
|
+
for (const [key, entry] of this.entries) {
|
|
114
|
+
if (entry.resetAt < oldestResetAt) {
|
|
115
|
+
oldestResetAt = entry.resetAt;
|
|
116
|
+
oldestKey = key;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (oldestKey !== null) this.entries.delete(oldestKey);
|
|
120
|
+
}
|
|
89
121
|
async increment(key, windowMs) {
|
|
90
122
|
const now = Date.now();
|
|
91
123
|
const current = this.entries.get(key);
|
|
92
124
|
const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
|
|
125
|
+
if (!current && this.entries.size >= this.maxKeys) {
|
|
126
|
+
this.sweepExpired(now);
|
|
127
|
+
if (this.entries.size >= this.maxKeys) this.evictOldest();
|
|
128
|
+
}
|
|
93
129
|
this.entries.set(key, next);
|
|
94
130
|
return next;
|
|
95
131
|
}
|
|
@@ -173,6 +209,23 @@ function principalRateKey(user) {
|
|
|
173
209
|
const collection = typeof user.collection === "string" && user.collection ? user.collection : "unknown";
|
|
174
210
|
return `${collection}:${String(user.id)}`;
|
|
175
211
|
}
|
|
212
|
+
var MAX_IP_KEY_LENGTH = 45;
|
|
213
|
+
var IPV4_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
214
|
+
var IPV6_PATTERN = /^[0-9a-fA-F:.%]+$/;
|
|
215
|
+
function clientIpRateKey(req) {
|
|
216
|
+
const candidate = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim() || "";
|
|
217
|
+
return normalizeIpKey(candidate);
|
|
218
|
+
}
|
|
219
|
+
function normalizeIpKey(candidate) {
|
|
220
|
+
if (!candidate || candidate.length > MAX_IP_KEY_LENGTH) return "unknown";
|
|
221
|
+
const host = candidate.startsWith("[") && candidate.endsWith("]") ? candidate.slice(1, -1) : candidate;
|
|
222
|
+
if (!host) return "unknown";
|
|
223
|
+
if (IPV4_PATTERN.test(host)) {
|
|
224
|
+
return host.split(".").every((octet) => Number(octet) <= 255) ? host : "unknown";
|
|
225
|
+
}
|
|
226
|
+
if (host.includes(":") && IPV6_PATTERN.test(host)) return host.toLowerCase();
|
|
227
|
+
return "unknown";
|
|
228
|
+
}
|
|
176
229
|
var RateLimiter = class {
|
|
177
230
|
/**
|
|
178
231
|
* @param namespace Endpoint-scoped prefix for every key this limiter writes.
|
|
@@ -252,7 +305,7 @@ function createInboundEmailEndpoint(capability, store) {
|
|
|
252
305
|
if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
|
|
253
306
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
254
307
|
}
|
|
255
|
-
const ip =
|
|
308
|
+
const ip = clientIpRateKey(req);
|
|
256
309
|
if (await limiter.check(ip, req)) {
|
|
257
310
|
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
258
311
|
}
|
|
@@ -2809,6 +2862,10 @@ function createStatusesEndpoint(slugs) {
|
|
|
2809
2862
|
if (!req.user) {
|
|
2810
2863
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2811
2864
|
}
|
|
2865
|
+
const collection = req.user.collection;
|
|
2866
|
+
if (collection !== slugs.users && collection !== slugs.supportClients) {
|
|
2867
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
2868
|
+
}
|
|
2812
2869
|
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2813
2870
|
sort: "sortOrder",
|
|
2814
2871
|
limit: 100,
|
|
@@ -2945,6 +3002,51 @@ function createApplyMacroEndpoint(slugs) {
|
|
|
2945
3002
|
};
|
|
2946
3003
|
}
|
|
2947
3004
|
|
|
3005
|
+
// src/utils/retention.ts
|
|
3006
|
+
var DEFAULT_RETENTION = {
|
|
3007
|
+
authLogsDays: 180,
|
|
3008
|
+
emailLogsDays: 365,
|
|
3009
|
+
cron: "0 30 3 * * *",
|
|
3010
|
+
queue: "default"
|
|
3011
|
+
};
|
|
3012
|
+
var PURGE_LOGS_TASK_SLUG = "support-purge-logs";
|
|
3013
|
+
function purgeableCollections(slugs) {
|
|
3014
|
+
return {
|
|
3015
|
+
"email-logs": slugs.emailLogs,
|
|
3016
|
+
"auth-logs": slugs.authLogs
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
async function purgeOlderThan(payload, slug, days, req) {
|
|
3020
|
+
const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
|
|
3021
|
+
const result = await dbDelete(payload, slug, {
|
|
3022
|
+
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
3023
|
+
overrideAccess: true,
|
|
3024
|
+
...req ? { req } : {}
|
|
3025
|
+
});
|
|
3026
|
+
return Array.isArray(result?.docs) ? result.docs.length : 0;
|
|
3027
|
+
}
|
|
3028
|
+
async function runScheduledPurge(payload, slugs, retention, req) {
|
|
3029
|
+
if (retention === false) return { purged: {}, skipped: ["auth-logs", "email-logs"] };
|
|
3030
|
+
const plan = [
|
|
3031
|
+
["auth-logs", slugs.authLogs, retention?.authLogsDays ?? DEFAULT_RETENTION.authLogsDays],
|
|
3032
|
+
["email-logs", slugs.emailLogs, retention?.emailLogsDays ?? DEFAULT_RETENTION.emailLogsDays]
|
|
3033
|
+
];
|
|
3034
|
+
const purged = {};
|
|
3035
|
+
const skipped = [];
|
|
3036
|
+
for (const [name, slug, days] of plan) {
|
|
3037
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
3038
|
+
skipped.push(name);
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
if (!payload.collections?.[slug]) {
|
|
3042
|
+
skipped.push(name);
|
|
3043
|
+
continue;
|
|
3044
|
+
}
|
|
3045
|
+
purged[name] = await purgeOlderThan(payload, slug, days, req);
|
|
3046
|
+
}
|
|
3047
|
+
return { purged, skipped };
|
|
3048
|
+
}
|
|
3049
|
+
|
|
2948
3050
|
// src/endpoints/purge-logs.ts
|
|
2949
3051
|
function createPurgeLogsEndpoint(slugs) {
|
|
2950
3052
|
return {
|
|
@@ -2957,19 +3059,11 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2957
3059
|
const url = new URL(req.url);
|
|
2958
3060
|
const collection = url.searchParams.get("collection");
|
|
2959
3061
|
const days = Number(url.searchParams.get("days") || "0");
|
|
2960
|
-
const allowedCollections =
|
|
2961
|
-
"email-logs": slugs.emailLogs,
|
|
2962
|
-
"auth-logs": slugs.authLogs
|
|
2963
|
-
};
|
|
3062
|
+
const allowedCollections = purgeableCollections(slugs);
|
|
2964
3063
|
if (!collection || !allowedCollections[collection]) {
|
|
2965
3064
|
return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
|
|
2966
3065
|
}
|
|
2967
|
-
const
|
|
2968
|
-
const result = await dbDelete(payload, allowedCollections[collection], {
|
|
2969
|
-
where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
|
|
2970
|
-
overrideAccess: true
|
|
2971
|
-
});
|
|
2972
|
-
const count = Array.isArray(result.docs) ? result.docs.length : 0;
|
|
3066
|
+
const count = await purgeOlderThan(payload, allowedCollections[collection], days, req);
|
|
2973
3067
|
return Response.json({
|
|
2974
3068
|
purged: count,
|
|
2975
3069
|
collection,
|
|
@@ -2999,7 +3093,7 @@ function createChatbotEndpoint(slugs, store, maxPerHour) {
|
|
|
2999
3093
|
method: "post",
|
|
3000
3094
|
handler: async (req) => {
|
|
3001
3095
|
try {
|
|
3002
|
-
const ip =
|
|
3096
|
+
const ip = clientIpRateKey(req);
|
|
3003
3097
|
if (await chatbotLimiter.check(ip, req)) {
|
|
3004
3098
|
return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
|
|
3005
3099
|
}
|
|
@@ -4692,6 +4786,7 @@ function createExportCsvEndpoint(slugs) {
|
|
|
4692
4786
|
}
|
|
4693
4787
|
|
|
4694
4788
|
// src/endpoints/export-data.ts
|
|
4789
|
+
var EMPTY = { docs: [] };
|
|
4695
4790
|
function createExportDataEndpoint(slugs) {
|
|
4696
4791
|
return {
|
|
4697
4792
|
path: "/support/export-data",
|
|
@@ -4700,7 +4795,16 @@ function createExportDataEndpoint(slugs) {
|
|
|
4700
4795
|
try {
|
|
4701
4796
|
const payload = req.payload;
|
|
4702
4797
|
requireClient(req, slugs);
|
|
4703
|
-
const
|
|
4798
|
+
const isRegistered = (slug) => Boolean(payload.collections?.[slug]);
|
|
4799
|
+
const [
|
|
4800
|
+
clientData,
|
|
4801
|
+
ticketsResult,
|
|
4802
|
+
messagesResult,
|
|
4803
|
+
surveysResult,
|
|
4804
|
+
chatResult,
|
|
4805
|
+
feedbackResult,
|
|
4806
|
+
summariesResult
|
|
4807
|
+
] = await Promise.all([
|
|
4704
4808
|
dbFindByID(payload, slugs.supportClients, {
|
|
4705
4809
|
id: req.user.id,
|
|
4706
4810
|
depth: 0,
|
|
@@ -4726,12 +4830,35 @@ function createExportDataEndpoint(slugs) {
|
|
|
4726
4830
|
limit: 500,
|
|
4727
4831
|
depth: 0,
|
|
4728
4832
|
overrideAccess: true
|
|
4729
|
-
})
|
|
4833
|
+
}),
|
|
4834
|
+
isRegistered(slugs.chatMessages) ? dbFind(payload, slugs.chatMessages, {
|
|
4835
|
+
where: { client: { equals: req.user.id } },
|
|
4836
|
+
limit: 5e3,
|
|
4837
|
+
depth: 0,
|
|
4838
|
+
overrideAccess: true
|
|
4839
|
+
}) : EMPTY,
|
|
4840
|
+
isRegistered(FIXED_SLUGS.ticketFeedback) ? dbFind(payload, FIXED_SLUGS.ticketFeedback, {
|
|
4841
|
+
where: { client: { equals: req.user.id } },
|
|
4842
|
+
limit: 500,
|
|
4843
|
+
depth: 0,
|
|
4844
|
+
overrideAccess: true
|
|
4845
|
+
}) : EMPTY,
|
|
4846
|
+
isRegistered(FIXED_SLUGS.clientSummaries) ? dbFind(payload, FIXED_SLUGS.clientSummaries, {
|
|
4847
|
+
where: { client: { equals: req.user.id } },
|
|
4848
|
+
limit: 100,
|
|
4849
|
+
depth: 0,
|
|
4850
|
+
overrideAccess: true
|
|
4851
|
+
}) : EMPTY
|
|
4730
4852
|
]);
|
|
4853
|
+
const ticketIds = ticketsResult.docs.map((t) => t.id);
|
|
4854
|
+
const timeResult = ticketIds.length > 0 && isRegistered(slugs.timeEntries) ? await dbFind(payload, slugs.timeEntries, {
|
|
4855
|
+
where: { ticket: { in: ticketIds } },
|
|
4856
|
+
limit: 5e3,
|
|
4857
|
+
depth: 0,
|
|
4858
|
+
overrideAccess: true
|
|
4859
|
+
}) : EMPTY;
|
|
4731
4860
|
const c = clientData;
|
|
4732
|
-
const
|
|
4733
|
-
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4734
|
-
exportType: "RGPD - Export des donn\xE9es personnelles",
|
|
4861
|
+
const providedByYou = {
|
|
4735
4862
|
profile: {
|
|
4736
4863
|
email: c.email,
|
|
4737
4864
|
firstName: c.firstName,
|
|
@@ -4760,8 +4887,50 @@ function createExportDataEndpoint(slugs) {
|
|
|
4760
4887
|
rating: s.rating,
|
|
4761
4888
|
comment: s.comment,
|
|
4762
4889
|
createdAt: s.createdAt
|
|
4890
|
+
})),
|
|
4891
|
+
feedback: feedbackResult.docs.map((f) => ({
|
|
4892
|
+
ticketId: f.ticket,
|
|
4893
|
+
rating: f.rating,
|
|
4894
|
+
comment: f.comment,
|
|
4895
|
+
submittedFrom: f.submittedFrom,
|
|
4896
|
+
createdAt: f.createdAt
|
|
4897
|
+
})),
|
|
4898
|
+
chatMessages: chatResult.docs.map((m) => ({
|
|
4899
|
+
session: m.session,
|
|
4900
|
+
senderType: m.senderType,
|
|
4901
|
+
message: m.message,
|
|
4902
|
+
createdAt: m.createdAt
|
|
4903
|
+
})),
|
|
4904
|
+
timeEntries: timeResult.docs.map((e) => ({
|
|
4905
|
+
ticketId: e.ticket,
|
|
4906
|
+
duration: e.duration,
|
|
4907
|
+
date: e.date,
|
|
4908
|
+
description: e.description,
|
|
4909
|
+
billable: e.billable
|
|
4763
4910
|
}))
|
|
4764
4911
|
};
|
|
4912
|
+
const exportData = {
|
|
4913
|
+
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4914
|
+
exportType: "RGPD - Export des donn\xE9es personnelles",
|
|
4915
|
+
providedByYou: {
|
|
4916
|
+
legalBasis: "Art. 20 RGPD - droit \xE0 la portabilit\xE9",
|
|
4917
|
+
...providedByYou
|
|
4918
|
+
},
|
|
4919
|
+
derivedData: {
|
|
4920
|
+
legalBasis: "Art. 15 RGPD - droit d'acc\xE8s (donn\xE9es d\xE9riv\xE9es, hors portabilit\xE9)",
|
|
4921
|
+
clientSummaries: summariesResult.docs.map((s) => ({
|
|
4922
|
+
summary: s.summary,
|
|
4923
|
+
recurringTopics: s.recurringTopics,
|
|
4924
|
+
patterns: s.patterns,
|
|
4925
|
+
keyFacts: s.keyFacts,
|
|
4926
|
+
ticketCount: s.ticketCount,
|
|
4927
|
+
messageCount: s.messageCount,
|
|
4928
|
+
averageSatisfaction: s.averageSatisfaction,
|
|
4929
|
+
generatedAt: s.generatedAt,
|
|
4930
|
+
aiModel: s.aiModel
|
|
4931
|
+
}))
|
|
4932
|
+
}
|
|
4933
|
+
};
|
|
4765
4934
|
const json = JSON.stringify(exportData, null, 2);
|
|
4766
4935
|
return new Response(json, {
|
|
4767
4936
|
status: 200,
|
|
@@ -5205,13 +5374,14 @@ function verifyTwoFactorChallenge(email, token, now = Date.now()) {
|
|
|
5205
5374
|
}
|
|
5206
5375
|
|
|
5207
5376
|
// src/endpoints/login.ts
|
|
5377
|
+
var MAX_LOGGED_USER_AGENT = 256;
|
|
5208
5378
|
function createLoginEndpoint(slugs, store) {
|
|
5209
5379
|
const loginLimiter = new RateLimiter(15 * 6e4, 10, store, "login");
|
|
5210
5380
|
return {
|
|
5211
5381
|
path: "/support/login",
|
|
5212
5382
|
method: "post",
|
|
5213
5383
|
handler: async (req) => {
|
|
5214
|
-
const ip =
|
|
5384
|
+
const ip = clientIpRateKey(req);
|
|
5215
5385
|
if (await loginLimiter.check(ip, req)) {
|
|
5216
5386
|
return Response.json(
|
|
5217
5387
|
{ error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
|
|
@@ -5226,7 +5396,7 @@ function createLoginEndpoint(slugs, store) {
|
|
|
5226
5396
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5227
5397
|
}
|
|
5228
5398
|
const { email, password } = body;
|
|
5229
|
-
const userAgent = req.headers.get("user-agent") || "";
|
|
5399
|
+
const userAgent = (req.headers.get("user-agent") || "").slice(0, MAX_LOGGED_USER_AGENT);
|
|
5230
5400
|
if (!email || !password) {
|
|
5231
5401
|
return Response.json({ error: "Email et mot de passe requis." }, { status: 400 });
|
|
5232
5402
|
}
|
|
@@ -5314,7 +5484,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5314
5484
|
if (!action || !email) {
|
|
5315
5485
|
return Response.json({ error: "Param\xE8tres manquants" }, { status: 400 });
|
|
5316
5486
|
}
|
|
5317
|
-
const
|
|
5487
|
+
const limiterKey = normalizeEmail(email);
|
|
5488
|
+
const sendResponse = () => {
|
|
5489
|
+
let refreshed;
|
|
5490
|
+
try {
|
|
5491
|
+
refreshed = issueTwoFactorChallenge(email);
|
|
5492
|
+
} catch {
|
|
5493
|
+
}
|
|
5494
|
+
return Response.json({
|
|
5495
|
+
success: true,
|
|
5496
|
+
message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9.",
|
|
5497
|
+
...refreshed ? { challenge: refreshed } : {}
|
|
5498
|
+
});
|
|
5499
|
+
};
|
|
5318
5500
|
if (action === "send") {
|
|
5319
5501
|
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5320
5502
|
return Response.json(
|
|
@@ -5322,8 +5504,8 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5322
5504
|
{ status: 401 }
|
|
5323
5505
|
);
|
|
5324
5506
|
}
|
|
5325
|
-
if (await sendLimiter.check(
|
|
5326
|
-
return
|
|
5507
|
+
if (await sendLimiter.check(limiterKey, req)) {
|
|
5508
|
+
return sendResponse();
|
|
5327
5509
|
}
|
|
5328
5510
|
const clients = await dbFind(payload, slugs.supportClients, {
|
|
5329
5511
|
where: { email: { equals: email } },
|
|
@@ -5332,7 +5514,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5332
5514
|
overrideAccess: true
|
|
5333
5515
|
});
|
|
5334
5516
|
if (clients.docs.length === 0) {
|
|
5335
|
-
return
|
|
5517
|
+
return sendResponse();
|
|
5336
5518
|
}
|
|
5337
5519
|
const client = clients.docs[0];
|
|
5338
5520
|
const plainCode = generateSecureCode();
|
|
@@ -5357,13 +5539,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5357
5539
|
<p style="font-size: 13px; color: #6b7280;">Ce code est valable 10 minutes.</p>
|
|
5358
5540
|
</div>`
|
|
5359
5541
|
});
|
|
5360
|
-
return
|
|
5542
|
+
return sendResponse();
|
|
5361
5543
|
}
|
|
5362
5544
|
if (action === "verify") {
|
|
5545
|
+
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5546
|
+
return Response.json(
|
|
5547
|
+
{ error: "Authentification requise avant la v\xE9rification d'un code." },
|
|
5548
|
+
{ status: 401 }
|
|
5549
|
+
);
|
|
5550
|
+
}
|
|
5363
5551
|
if (!code) {
|
|
5364
5552
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
5365
5553
|
}
|
|
5366
|
-
if (await verifyLimiter.check(
|
|
5554
|
+
if (await verifyLimiter.check(limiterKey, req)) {
|
|
5367
5555
|
return Response.json(
|
|
5368
5556
|
{ error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
|
|
5369
5557
|
{ status: 429 }
|
|
@@ -5404,7 +5592,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5404
5592
|
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
5405
5593
|
overrideAccess: true
|
|
5406
5594
|
});
|
|
5407
|
-
verifyLimiter.reset(
|
|
5595
|
+
await verifyLimiter.reset(limiterKey, req);
|
|
5408
5596
|
return Response.json({ success: true, verified: true });
|
|
5409
5597
|
}
|
|
5410
5598
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -5656,15 +5844,22 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
5656
5844
|
}
|
|
5657
5845
|
};
|
|
5658
5846
|
}
|
|
5659
|
-
|
|
5660
|
-
|
|
5847
|
+
function collectAttachmentIds(docs, into) {
|
|
5848
|
+
for (const doc of docs) {
|
|
5849
|
+
for (const attachment of doc.attachments ?? []) {
|
|
5850
|
+
const file = attachment?.file;
|
|
5851
|
+
const id = file != null && typeof file === "object" ? file.id : file;
|
|
5852
|
+
if (id != null) into.add(id);
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5661
5856
|
function createDeleteAccountEndpoint(slugs) {
|
|
5662
5857
|
return {
|
|
5663
5858
|
path: "/support/delete-account",
|
|
5664
5859
|
method: "post",
|
|
5665
5860
|
handler: async (req) => {
|
|
5666
5861
|
try {
|
|
5667
|
-
const payload = req.payload;
|
|
5862
|
+
const payload$1 = req.payload;
|
|
5668
5863
|
requireClient(req, slugs);
|
|
5669
5864
|
const body = await req.json();
|
|
5670
5865
|
const { confirmPassword } = body;
|
|
@@ -5675,7 +5870,7 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
5675
5870
|
);
|
|
5676
5871
|
}
|
|
5677
5872
|
try {
|
|
5678
|
-
await payload.login({
|
|
5873
|
+
await payload$1.login({
|
|
5679
5874
|
collection: slugs.supportClients,
|
|
5680
5875
|
data: { email: req.user.email, password: confirmPassword }
|
|
5681
5876
|
});
|
|
@@ -5686,44 +5881,135 @@ function createDeleteAccountEndpoint(slugs) {
|
|
|
5686
5881
|
);
|
|
5687
5882
|
}
|
|
5688
5883
|
const clientId = req.user.id;
|
|
5689
|
-
const
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
select: { id: true }
|
|
5695
|
-
});
|
|
5696
|
-
const ticketIds = tickets.docs.map((t) => t.id);
|
|
5697
|
-
if (ticketIds.length > 0) {
|
|
5698
|
-
await dbDelete(payload, slugs.ticketMessages, {
|
|
5699
|
-
where: { ticket: { in: ticketIds } },
|
|
5700
|
-
overrideAccess: true
|
|
5701
|
-
});
|
|
5702
|
-
await dbDelete(payload, slugs.ticketActivityLog, {
|
|
5703
|
-
where: { ticket: { in: ticketIds } },
|
|
5704
|
-
overrideAccess: true
|
|
5705
|
-
});
|
|
5706
|
-
await dbDelete(payload, slugs.timeEntries, {
|
|
5707
|
-
where: { ticket: { in: ticketIds } },
|
|
5708
|
-
overrideAccess: true
|
|
5709
|
-
});
|
|
5710
|
-
await dbDelete(payload, slugs.tickets, {
|
|
5884
|
+
const clientEmail = req.user.email;
|
|
5885
|
+
const isRegistered = (slug) => Boolean(payload$1.collections?.[slug]);
|
|
5886
|
+
const startedTransaction = await payload.initTransaction(req);
|
|
5887
|
+
try {
|
|
5888
|
+
const tickets = await dbFind(payload$1, slugs.tickets, {
|
|
5711
5889
|
where: { client: { equals: clientId } },
|
|
5712
|
-
|
|
5890
|
+
limit: 1e4,
|
|
5891
|
+
depth: 0,
|
|
5892
|
+
overrideAccess: true,
|
|
5893
|
+
select: { id: true },
|
|
5894
|
+
req
|
|
5713
5895
|
});
|
|
5896
|
+
const ticketIds = tickets.docs.map((t) => t.id);
|
|
5897
|
+
const attachmentIds = /* @__PURE__ */ new Set();
|
|
5898
|
+
if (ticketIds.length > 0) {
|
|
5899
|
+
const messages = await dbFind(
|
|
5900
|
+
payload$1,
|
|
5901
|
+
slugs.ticketMessages,
|
|
5902
|
+
{
|
|
5903
|
+
where: { ticket: { in: ticketIds } },
|
|
5904
|
+
limit: 1e4,
|
|
5905
|
+
depth: 0,
|
|
5906
|
+
overrideAccess: true,
|
|
5907
|
+
req
|
|
5908
|
+
}
|
|
5909
|
+
);
|
|
5910
|
+
collectAttachmentIds(messages.docs, attachmentIds);
|
|
5911
|
+
}
|
|
5912
|
+
if (isRegistered(slugs.pendingEmails)) {
|
|
5913
|
+
const pendingEmails = await dbFind(
|
|
5914
|
+
payload$1,
|
|
5915
|
+
slugs.pendingEmails,
|
|
5916
|
+
{
|
|
5917
|
+
where: { client: { equals: clientId } },
|
|
5918
|
+
limit: 1e4,
|
|
5919
|
+
depth: 0,
|
|
5920
|
+
overrideAccess: true,
|
|
5921
|
+
req
|
|
5922
|
+
}
|
|
5923
|
+
);
|
|
5924
|
+
collectAttachmentIds(pendingEmails.docs, attachmentIds);
|
|
5925
|
+
}
|
|
5926
|
+
if (ticketIds.length > 0) {
|
|
5927
|
+
for (const slug of [FIXED_SLUGS.ticketFeedback, FIXED_SLUGS.ticketCollaborators]) {
|
|
5928
|
+
if (!isRegistered(slug)) continue;
|
|
5929
|
+
await dbDelete(payload$1, slug, {
|
|
5930
|
+
where: {
|
|
5931
|
+
or: [
|
|
5932
|
+
{ ticket: { in: ticketIds } },
|
|
5933
|
+
{ client: { equals: clientId } }
|
|
5934
|
+
]
|
|
5935
|
+
},
|
|
5936
|
+
overrideAccess: true,
|
|
5937
|
+
req
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
if (ticketIds.length > 0) {
|
|
5942
|
+
await dbDelete(payload$1, slugs.tickets, {
|
|
5943
|
+
where: { client: { equals: clientId } },
|
|
5944
|
+
overrideAccess: true,
|
|
5945
|
+
req
|
|
5946
|
+
});
|
|
5947
|
+
}
|
|
5948
|
+
for (const slug of [
|
|
5949
|
+
slugs.satisfactionSurveys,
|
|
5950
|
+
slugs.chatMessages,
|
|
5951
|
+
slugs.notificationQueue,
|
|
5952
|
+
slugs.pendingEmails,
|
|
5953
|
+
FIXED_SLUGS.clientSummaries,
|
|
5954
|
+
FIXED_SLUGS.ticketFeedback
|
|
5955
|
+
]) {
|
|
5956
|
+
if (!isRegistered(slug)) continue;
|
|
5957
|
+
await dbDelete(payload$1, slug, {
|
|
5958
|
+
where: { client: { equals: clientId } },
|
|
5959
|
+
overrideAccess: true,
|
|
5960
|
+
req
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5963
|
+
if (isRegistered(FIXED_SLUGS.ticketCollaborators)) {
|
|
5964
|
+
await dbDelete(payload$1, FIXED_SLUGS.ticketCollaborators, {
|
|
5965
|
+
where: {
|
|
5966
|
+
or: [
|
|
5967
|
+
{ client: { equals: clientId } },
|
|
5968
|
+
...clientEmail ? [{ email: { equals: clientEmail } }] : []
|
|
5969
|
+
]
|
|
5970
|
+
},
|
|
5971
|
+
overrideAccess: true,
|
|
5972
|
+
req
|
|
5973
|
+
});
|
|
5974
|
+
}
|
|
5975
|
+
if (clientEmail) {
|
|
5976
|
+
if (isRegistered(slugs.authLogs)) {
|
|
5977
|
+
await dbDelete(payload$1, slugs.authLogs, {
|
|
5978
|
+
where: { email: { equals: clientEmail } },
|
|
5979
|
+
overrideAccess: true,
|
|
5980
|
+
req
|
|
5981
|
+
});
|
|
5982
|
+
}
|
|
5983
|
+
if (isRegistered(slugs.emailLogs)) {
|
|
5984
|
+
await dbDelete(payload$1, slugs.emailLogs, {
|
|
5985
|
+
where: {
|
|
5986
|
+
or: [
|
|
5987
|
+
{ senderEmail: { equals: clientEmail } },
|
|
5988
|
+
{ recipientEmail: { equals: clientEmail } }
|
|
5989
|
+
]
|
|
5990
|
+
},
|
|
5991
|
+
overrideAccess: true,
|
|
5992
|
+
req
|
|
5993
|
+
});
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
for (const id of isRegistered(slugs.media) ? attachmentIds : []) {
|
|
5997
|
+
await dbDelete(payload$1, slugs.media, {
|
|
5998
|
+
id,
|
|
5999
|
+
overrideAccess: true,
|
|
6000
|
+
req
|
|
6001
|
+
});
|
|
6002
|
+
}
|
|
6003
|
+
await dbDelete(payload$1, slugs.supportClients, {
|
|
6004
|
+
id: clientId,
|
|
6005
|
+
overrideAccess: true,
|
|
6006
|
+
req
|
|
6007
|
+
});
|
|
6008
|
+
if (startedTransaction) await payload.commitTransaction(req);
|
|
6009
|
+
} catch (err) {
|
|
6010
|
+
if (startedTransaction) await payload.killTransaction(req);
|
|
6011
|
+
throw err;
|
|
5714
6012
|
}
|
|
5715
|
-
await dbDelete(payload, slugs.satisfactionSurveys, {
|
|
5716
|
-
where: { client: { equals: clientId } },
|
|
5717
|
-
overrideAccess: true
|
|
5718
|
-
});
|
|
5719
|
-
await dbDelete(payload, slugs.chatMessages, {
|
|
5720
|
-
where: { client: { equals: clientId } },
|
|
5721
|
-
overrideAccess: true
|
|
5722
|
-
});
|
|
5723
|
-
await dbDelete(payload, slugs.supportClients, {
|
|
5724
|
-
id: clientId,
|
|
5725
|
-
overrideAccess: true
|
|
5726
|
-
});
|
|
5727
6013
|
const headers = new Headers({ "Content-Type": "application/json" });
|
|
5728
6014
|
const secure = process.env.NODE_ENV === "production";
|
|
5729
6015
|
headers.append(
|
|
@@ -5966,7 +6252,7 @@ function createImportConversationEndpoint(slugs, store) {
|
|
|
5966
6252
|
if (!isAuthed) {
|
|
5967
6253
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5968
6254
|
}
|
|
5969
|
-
const ip = req
|
|
6255
|
+
const ip = clientIpRateKey(req);
|
|
5970
6256
|
if (await importLimiter.check(ip, req)) {
|
|
5971
6257
|
return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
|
|
5972
6258
|
}
|
|
@@ -6174,6 +6460,21 @@ var WEBHOOK_URL_MESSAGES = {
|
|
|
6174
6460
|
scheme_not_allowed: "Seules les URL https:// sont accept\xE9es.",
|
|
6175
6461
|
private_host: "Les adresses priv\xE9es, loopback et link-local sont interdites (SSRF)."
|
|
6176
6462
|
};
|
|
6463
|
+
var MAX_PUSH_ENDPOINT_LENGTH = 2048;
|
|
6464
|
+
function validatePushEndpoint(raw) {
|
|
6465
|
+
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_PUSH_ENDPOINT_LENGTH) {
|
|
6466
|
+
return { ok: false, reason: "invalid_url" };
|
|
6467
|
+
}
|
|
6468
|
+
let url;
|
|
6469
|
+
try {
|
|
6470
|
+
url = new URL(raw.trim());
|
|
6471
|
+
} catch {
|
|
6472
|
+
return { ok: false, reason: "invalid_url" };
|
|
6473
|
+
}
|
|
6474
|
+
if (url.protocol !== "https:") return { ok: false, reason: "scheme_not_allowed" };
|
|
6475
|
+
if (isBlockedHost(url.hostname)) return { ok: false, reason: "private_host" };
|
|
6476
|
+
return { ok: true, url };
|
|
6477
|
+
}
|
|
6177
6478
|
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
6178
6479
|
var LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("lookup-timed-out");
|
|
6179
6480
|
var NONEXISTENT_HOST_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA", "NOTFOUND"]);
|
|
@@ -6629,6 +6930,15 @@ async function sendPushToUser(payload, slugs, userId, notification) {
|
|
|
6629
6930
|
for (const s of subs.docs) {
|
|
6630
6931
|
const row = s;
|
|
6631
6932
|
if (!row.endpoint || !row.p256dh || !row.auth) continue;
|
|
6933
|
+
const check = validatePushEndpoint(row.endpoint);
|
|
6934
|
+
if (!check.ok || !check.url) {
|
|
6935
|
+
console.warn("[support] Push endpoint refused (unsafe URL):", check.reason);
|
|
6936
|
+
continue;
|
|
6937
|
+
}
|
|
6938
|
+
if (!await assertPublicHost(check.url.hostname)) {
|
|
6939
|
+
console.warn("[support] Push endpoint refused (host resolves to a private address)");
|
|
6940
|
+
continue;
|
|
6941
|
+
}
|
|
6632
6942
|
try {
|
|
6633
6943
|
await webpush__default.default.sendNotification(
|
|
6634
6944
|
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
|
@@ -6679,7 +6989,14 @@ function createPushSubscribeEndpoint(slugs) {
|
|
|
6679
6989
|
if (!endpoint || !p256dh || !auth) {
|
|
6680
6990
|
return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
|
|
6681
6991
|
}
|
|
6682
|
-
const
|
|
6992
|
+
const endpointCheck = validatePushEndpoint(endpoint);
|
|
6993
|
+
if (!endpointCheck.ok) {
|
|
6994
|
+
return Response.json(
|
|
6995
|
+
{ error: WEBHOOK_URL_MESSAGES[endpointCheck.reason || "invalid_url"] },
|
|
6996
|
+
{ status: 400 }
|
|
6997
|
+
);
|
|
6998
|
+
}
|
|
6999
|
+
const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: (req.headers.get("user-agent") || "").slice(0, 256) };
|
|
6683
7000
|
const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
|
|
6684
7001
|
if (existing.docs.length > 0) {
|
|
6685
7002
|
await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
|
|
@@ -8335,9 +8652,11 @@ function createCascadeDelete(slugs) {
|
|
|
8335
8652
|
return async ({ id, req }) => {
|
|
8336
8653
|
const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
|
|
8337
8654
|
for (const slug of collections) {
|
|
8655
|
+
if (!req.payload.collections?.[slug]) continue;
|
|
8338
8656
|
await dbDelete(req.payload, slug, {
|
|
8339
8657
|
where: { ticket: { equals: id } },
|
|
8340
|
-
overrideAccess: true
|
|
8658
|
+
overrideAccess: true,
|
|
8659
|
+
req
|
|
8341
8660
|
});
|
|
8342
8661
|
}
|
|
8343
8662
|
};
|
|
@@ -9825,7 +10144,7 @@ function createSupportClientsCollection(slugs, options) {
|
|
|
9825
10144
|
}
|
|
9826
10145
|
|
|
9827
10146
|
// src/collections/TimeEntries.ts
|
|
9828
|
-
async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
10147
|
+
async function recalculateTicketTime(payload, slugs, ticketId, req) {
|
|
9829
10148
|
let totalMinutes = 0;
|
|
9830
10149
|
let page = 1;
|
|
9831
10150
|
let hasMore = true;
|
|
@@ -9837,7 +10156,8 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
|
9837
10156
|
page,
|
|
9838
10157
|
depth: 0,
|
|
9839
10158
|
overrideAccess: true,
|
|
9840
|
-
select: { duration: true }
|
|
10159
|
+
select: { duration: true },
|
|
10160
|
+
req
|
|
9841
10161
|
});
|
|
9842
10162
|
for (const entry of entries.docs) {
|
|
9843
10163
|
totalMinutes += entry.duration || 0;
|
|
@@ -9849,14 +10169,15 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
|
|
|
9849
10169
|
collection: slugs.tickets,
|
|
9850
10170
|
id: ticketId,
|
|
9851
10171
|
data: { totalTimeMinutes: totalMinutes },
|
|
9852
|
-
overrideAccess: true
|
|
10172
|
+
overrideAccess: true,
|
|
10173
|
+
req
|
|
9853
10174
|
});
|
|
9854
10175
|
}
|
|
9855
10176
|
function createRecalculateTicketTime(slugs) {
|
|
9856
10177
|
return async ({ doc, req }) => {
|
|
9857
10178
|
if (!doc.ticket) return;
|
|
9858
10179
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9859
|
-
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
10180
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req);
|
|
9860
10181
|
};
|
|
9861
10182
|
}
|
|
9862
10183
|
function createRecalculateTicketTimeOnDelete(slugs) {
|
|
@@ -9864,7 +10185,7 @@ function createRecalculateTicketTimeOnDelete(slugs) {
|
|
|
9864
10185
|
if (!doc?.ticket) return;
|
|
9865
10186
|
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
9866
10187
|
try {
|
|
9867
|
-
await recalculateTicketTime(req.payload, slugs, ticketId);
|
|
10188
|
+
await recalculateTicketTime(req.payload, slugs, ticketId, req);
|
|
9868
10189
|
} catch (err) {
|
|
9869
10190
|
console.error("[support] Failed to recalculate ticket time after delete:", err);
|
|
9870
10191
|
}
|
|
@@ -11735,8 +12056,30 @@ function supportPlugin(config) {
|
|
|
11735
12056
|
rateLimitStore,
|
|
11736
12057
|
capabilities: config?.capabilities
|
|
11737
12058
|
});
|
|
12059
|
+
const retention = config?.retention === false ? void 0 : config?.retention;
|
|
12060
|
+
const retentionEnabled = Boolean(retention);
|
|
12061
|
+
const purgeTask = {
|
|
12062
|
+
slug: PURGE_LOGS_TASK_SLUG,
|
|
12063
|
+
label: "Support \u2014 purge des journaux",
|
|
12064
|
+
schedule: [
|
|
12065
|
+
{
|
|
12066
|
+
cron: retention?.cron ?? DEFAULT_RETENTION.cron,
|
|
12067
|
+
queue: retention?.queue ?? DEFAULT_RETENTION.queue
|
|
12068
|
+
}
|
|
12069
|
+
],
|
|
12070
|
+
handler: async ({ req }) => {
|
|
12071
|
+
const result = await runScheduledPurge(req.payload, slugs, retention, req);
|
|
12072
|
+
return { output: result };
|
|
12073
|
+
}
|
|
12074
|
+
};
|
|
12075
|
+
const existingJobs = incomingConfig.jobs;
|
|
12076
|
+
const jobs = retentionEnabled ? {
|
|
12077
|
+
...existingJobs,
|
|
12078
|
+
tasks: [...existingJobs?.tasks ?? [], purgeTask]
|
|
12079
|
+
} : existingJobs;
|
|
11738
12080
|
return {
|
|
11739
12081
|
...incomingConfig,
|
|
12082
|
+
jobs,
|
|
11740
12083
|
// Publish the resolved staff collection so the server-side readers share
|
|
11741
12084
|
// ONE source of truth with the writers. `requireAdmin` compares against
|
|
11742
12085
|
// `slugs.users`; the `payload-preferences` reads used to scope themselves
|
|
@@ -11763,11 +12106,13 @@ function supportPlugin(config) {
|
|
|
11763
12106
|
|
|
11764
12107
|
exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
11765
12108
|
exports.DEFAULT_INBOUND_EMAIL_LIMITS = DEFAULT_INBOUND_EMAIL_LIMITS;
|
|
12109
|
+
exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
|
|
11766
12110
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
11767
12111
|
exports.DEFAULT_SLUGS = DEFAULT_SLUGS;
|
|
11768
12112
|
exports.DEFAULT_TICKETING_FEATURES = DEFAULT_TICKETING_FEATURES;
|
|
11769
12113
|
exports.DEFAULT_USER_PREFS = DEFAULT_USER_PREFS;
|
|
11770
12114
|
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
12115
|
+
exports.PURGE_LOGS_TASK_SLUG = PURGE_LOGS_TASK_SLUG;
|
|
11771
12116
|
exports.PayloadRateLimitStore = PayloadRateLimitStore;
|
|
11772
12117
|
exports.RateLimiter = RateLimiter;
|
|
11773
12118
|
exports.calculateBusinessHoursDeadline = calculateBusinessHoursDeadline;
|
|
@@ -11800,10 +12145,13 @@ exports.dispatchWebhook = dispatchWebhook;
|
|
|
11800
12145
|
exports.generateTicketSynthesis = generateTicketSynthesis;
|
|
11801
12146
|
exports.normalizeFeatures = normalizeFeatures;
|
|
11802
12147
|
exports.projectAutoClose = projectAutoClose;
|
|
12148
|
+
exports.purgeOlderThan = purgeOlderThan;
|
|
12149
|
+
exports.purgeableCollections = purgeableCollections;
|
|
11803
12150
|
exports.readSupportSettings = readSupportSettings;
|
|
11804
12151
|
exports.readSupportSettingsState = readSupportSettingsState;
|
|
11805
12152
|
exports.readUserPrefs = readUserPrefs;
|
|
11806
12153
|
exports.resolveSlugs = resolveSlugs;
|
|
12154
|
+
exports.runScheduledPurge = runScheduledPurge;
|
|
11807
12155
|
exports.stripProjectedFeatures = stripProjectedFeatures;
|
|
11808
12156
|
exports.supportPlugin = supportPlugin;
|
|
11809
12157
|
exports.validateInboundEmailPayload = validateInboundEmailPayload;
|