@consilioweb/payload-support 4.0.0 → 5.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 +24 -14
- package/dist/index.cjs +115 -13
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +115 -13
- package/dist/utils/db.d.ts +34 -0
- package/dist/utils/readSettings.d.ts +90 -0
- package/dist/views/BillingView/index.js +4 -4
- package/dist/views/ChatView/index.js +4 -4
- package/dist/views/CrmView/index.js +4 -4
- package/dist/views/EmailTrackingView/index.js +4 -4
- package/dist/views/ImportConversationView/index.js +4 -4
- package/dist/views/LogsView/index.js +4 -2
- package/dist/views/NewTicketView/index.js +4 -2
- package/dist/views/PendingEmailsView/index.js +4 -4
- package/dist/views/SupportDashboardView/index.js +4 -4
- package/dist/views/TicketDetailView/index.js +4 -4
- package/dist/views/TicketInboxView/index.js +4 -2
- package/dist/views/TicketingSettingsView/index.js +4 -4
- package/dist/views/TimeDashboardView/index.js +4 -4
- package/dist/views/shared/viewAccess.d.ts +29 -0
- package/dist/views/shared/viewAccess.js +24 -0
- package/package.json +26 -20
- 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/import-conversation.ts +2 -2
- package/src/endpoints/login.ts +13 -3
- package/src/endpoints/push.ts +14 -1
- package/src/endpoints/statuses.ts +17 -0
- package/src/portal/login/page.tsx +14 -4
- package/src/utils/push.ts +22 -0
- package/src/utils/rateLimiter.ts +98 -0
- package/src/utils/twoFactorChallenge.ts +6 -1
- package/src/utils/urlSafety.ts +36 -1
- package/src/views/BillingView/index.tsx +4 -4
- package/src/views/ChatView/index.tsx +4 -4
- package/src/views/CrmView/index.tsx +4 -4
- package/src/views/EmailTrackingView/index.tsx +4 -4
- package/src/views/ImportConversationView/index.tsx +4 -4
- package/src/views/LogsView/index.tsx +4 -2
- package/src/views/NewTicketView/index.tsx +4 -2
- package/src/views/PendingEmailsView/index.tsx +4 -4
- package/src/views/SupportDashboardView/index.tsx +4 -4
- package/src/views/TicketDetailView/index.tsx +4 -4
- package/src/views/TicketInboxView/index.tsx +4 -2
- package/src/views/TicketingSettingsView/index.tsx +4 -4
- package/src/views/TimeDashboardView/index.tsx +4 -4
- package/src/views/shared/viewAccess.ts +73 -0
package/dist/index.js
CHANGED
|
@@ -75,12 +75,43 @@ var DEFAULT_SLUGS = {
|
|
|
75
75
|
function resolveSlugs(overrides) {
|
|
76
76
|
return { ...DEFAULT_SLUGS, ...overrides };
|
|
77
77
|
}
|
|
78
|
+
var MAX_MEMORY_RATE_LIMIT_KEYS = 1e4;
|
|
78
79
|
var MemoryRateLimitStore = class {
|
|
80
|
+
constructor(maxKeys = MAX_MEMORY_RATE_LIMIT_KEYS) {
|
|
81
|
+
this.maxKeys = maxKeys;
|
|
82
|
+
}
|
|
83
|
+
maxKeys;
|
|
79
84
|
entries = /* @__PURE__ */ new Map();
|
|
85
|
+
/** Distinct keys currently held. Exposed so the ceiling can be asserted. */
|
|
86
|
+
get size() {
|
|
87
|
+
return this.entries.size;
|
|
88
|
+
}
|
|
89
|
+
/** Reclaims every window that has already closed. */
|
|
90
|
+
sweepExpired(now) {
|
|
91
|
+
for (const [key, entry] of this.entries) {
|
|
92
|
+
if (now > entry.resetAt) this.entries.delete(key);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Soonest-closing window first, so the ceiling drops the least useful entry. */
|
|
96
|
+
evictOldest() {
|
|
97
|
+
let oldestKey = null;
|
|
98
|
+
let oldestResetAt = Infinity;
|
|
99
|
+
for (const [key, entry] of this.entries) {
|
|
100
|
+
if (entry.resetAt < oldestResetAt) {
|
|
101
|
+
oldestResetAt = entry.resetAt;
|
|
102
|
+
oldestKey = key;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (oldestKey !== null) this.entries.delete(oldestKey);
|
|
106
|
+
}
|
|
80
107
|
async increment(key, windowMs) {
|
|
81
108
|
const now = Date.now();
|
|
82
109
|
const current = this.entries.get(key);
|
|
83
110
|
const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
|
|
111
|
+
if (!current && this.entries.size >= this.maxKeys) {
|
|
112
|
+
this.sweepExpired(now);
|
|
113
|
+
if (this.entries.size >= this.maxKeys) this.evictOldest();
|
|
114
|
+
}
|
|
84
115
|
this.entries.set(key, next);
|
|
85
116
|
return next;
|
|
86
117
|
}
|
|
@@ -164,6 +195,23 @@ function principalRateKey(user) {
|
|
|
164
195
|
const collection = typeof user.collection === "string" && user.collection ? user.collection : "unknown";
|
|
165
196
|
return `${collection}:${String(user.id)}`;
|
|
166
197
|
}
|
|
198
|
+
var MAX_IP_KEY_LENGTH = 45;
|
|
199
|
+
var IPV4_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/;
|
|
200
|
+
var IPV6_PATTERN = /^[0-9a-fA-F:.%]+$/;
|
|
201
|
+
function clientIpRateKey(req) {
|
|
202
|
+
const candidate = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim() || "";
|
|
203
|
+
return normalizeIpKey(candidate);
|
|
204
|
+
}
|
|
205
|
+
function normalizeIpKey(candidate) {
|
|
206
|
+
if (!candidate || candidate.length > MAX_IP_KEY_LENGTH) return "unknown";
|
|
207
|
+
const host = candidate.startsWith("[") && candidate.endsWith("]") ? candidate.slice(1, -1) : candidate;
|
|
208
|
+
if (!host) return "unknown";
|
|
209
|
+
if (IPV4_PATTERN.test(host)) {
|
|
210
|
+
return host.split(".").every((octet) => Number(octet) <= 255) ? host : "unknown";
|
|
211
|
+
}
|
|
212
|
+
if (host.includes(":") && IPV6_PATTERN.test(host)) return host.toLowerCase();
|
|
213
|
+
return "unknown";
|
|
214
|
+
}
|
|
167
215
|
var RateLimiter = class {
|
|
168
216
|
/**
|
|
169
217
|
* @param namespace Endpoint-scoped prefix for every key this limiter writes.
|
|
@@ -243,7 +291,7 @@ function createInboundEmailEndpoint(capability, store) {
|
|
|
243
291
|
if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
|
|
244
292
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
245
293
|
}
|
|
246
|
-
const ip =
|
|
294
|
+
const ip = clientIpRateKey(req);
|
|
247
295
|
if (await limiter.check(ip, req)) {
|
|
248
296
|
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
249
297
|
}
|
|
@@ -2800,6 +2848,10 @@ function createStatusesEndpoint(slugs) {
|
|
|
2800
2848
|
if (!req.user) {
|
|
2801
2849
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
2802
2850
|
}
|
|
2851
|
+
const collection = req.user.collection;
|
|
2852
|
+
if (collection !== slugs.users && collection !== slugs.supportClients) {
|
|
2853
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
2854
|
+
}
|
|
2803
2855
|
const { docs } = await dbFind(payload, slugs.ticketStatuses, {
|
|
2804
2856
|
sort: "sortOrder",
|
|
2805
2857
|
limit: 100,
|
|
@@ -2990,7 +3042,7 @@ function createChatbotEndpoint(slugs, store, maxPerHour) {
|
|
|
2990
3042
|
method: "post",
|
|
2991
3043
|
handler: async (req) => {
|
|
2992
3044
|
try {
|
|
2993
|
-
const ip =
|
|
3045
|
+
const ip = clientIpRateKey(req);
|
|
2994
3046
|
if (await chatbotLimiter.check(ip, req)) {
|
|
2995
3047
|
return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
|
|
2996
3048
|
}
|
|
@@ -5196,13 +5248,14 @@ function verifyTwoFactorChallenge(email, token, now = Date.now()) {
|
|
|
5196
5248
|
}
|
|
5197
5249
|
|
|
5198
5250
|
// src/endpoints/login.ts
|
|
5251
|
+
var MAX_LOGGED_USER_AGENT = 256;
|
|
5199
5252
|
function createLoginEndpoint(slugs, store) {
|
|
5200
5253
|
const loginLimiter = new RateLimiter(15 * 6e4, 10, store, "login");
|
|
5201
5254
|
return {
|
|
5202
5255
|
path: "/support/login",
|
|
5203
5256
|
method: "post",
|
|
5204
5257
|
handler: async (req) => {
|
|
5205
|
-
const ip =
|
|
5258
|
+
const ip = clientIpRateKey(req);
|
|
5206
5259
|
if (await loginLimiter.check(ip, req)) {
|
|
5207
5260
|
return Response.json(
|
|
5208
5261
|
{ error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
|
|
@@ -5217,7 +5270,7 @@ function createLoginEndpoint(slugs, store) {
|
|
|
5217
5270
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
5218
5271
|
}
|
|
5219
5272
|
const { email, password } = body;
|
|
5220
|
-
const userAgent = req.headers.get("user-agent") || "";
|
|
5273
|
+
const userAgent = (req.headers.get("user-agent") || "").slice(0, MAX_LOGGED_USER_AGENT);
|
|
5221
5274
|
if (!email || !password) {
|
|
5222
5275
|
return Response.json({ error: "Email et mot de passe requis." }, { status: 400 });
|
|
5223
5276
|
}
|
|
@@ -5305,7 +5358,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5305
5358
|
if (!action || !email) {
|
|
5306
5359
|
return Response.json({ error: "Param\xE8tres manquants" }, { status: 400 });
|
|
5307
5360
|
}
|
|
5308
|
-
const
|
|
5361
|
+
const limiterKey = normalizeEmail(email);
|
|
5362
|
+
const sendResponse = () => {
|
|
5363
|
+
let refreshed;
|
|
5364
|
+
try {
|
|
5365
|
+
refreshed = issueTwoFactorChallenge(email);
|
|
5366
|
+
} catch {
|
|
5367
|
+
}
|
|
5368
|
+
return Response.json({
|
|
5369
|
+
success: true,
|
|
5370
|
+
message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9.",
|
|
5371
|
+
...refreshed ? { challenge: refreshed } : {}
|
|
5372
|
+
});
|
|
5373
|
+
};
|
|
5309
5374
|
if (action === "send") {
|
|
5310
5375
|
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5311
5376
|
return Response.json(
|
|
@@ -5313,8 +5378,8 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5313
5378
|
{ status: 401 }
|
|
5314
5379
|
);
|
|
5315
5380
|
}
|
|
5316
|
-
if (await sendLimiter.check(
|
|
5317
|
-
return
|
|
5381
|
+
if (await sendLimiter.check(limiterKey, req)) {
|
|
5382
|
+
return sendResponse();
|
|
5318
5383
|
}
|
|
5319
5384
|
const clients = await dbFind(payload, slugs.supportClients, {
|
|
5320
5385
|
where: { email: { equals: email } },
|
|
@@ -5323,7 +5388,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5323
5388
|
overrideAccess: true
|
|
5324
5389
|
});
|
|
5325
5390
|
if (clients.docs.length === 0) {
|
|
5326
|
-
return
|
|
5391
|
+
return sendResponse();
|
|
5327
5392
|
}
|
|
5328
5393
|
const client = clients.docs[0];
|
|
5329
5394
|
const plainCode = generateSecureCode();
|
|
@@ -5348,13 +5413,19 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5348
5413
|
<p style="font-size: 13px; color: #6b7280;">Ce code est valable 10 minutes.</p>
|
|
5349
5414
|
</div>`
|
|
5350
5415
|
});
|
|
5351
|
-
return
|
|
5416
|
+
return sendResponse();
|
|
5352
5417
|
}
|
|
5353
5418
|
if (action === "verify") {
|
|
5419
|
+
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
5420
|
+
return Response.json(
|
|
5421
|
+
{ error: "Authentification requise avant la v\xE9rification d'un code." },
|
|
5422
|
+
{ status: 401 }
|
|
5423
|
+
);
|
|
5424
|
+
}
|
|
5354
5425
|
if (!code) {
|
|
5355
5426
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
5356
5427
|
}
|
|
5357
|
-
if (await verifyLimiter.check(
|
|
5428
|
+
if (await verifyLimiter.check(limiterKey, req)) {
|
|
5358
5429
|
return Response.json(
|
|
5359
5430
|
{ error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
|
|
5360
5431
|
{ status: 429 }
|
|
@@ -5395,7 +5466,7 @@ function createAuth2faEndpoint(slugs, store) {
|
|
|
5395
5466
|
data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
5396
5467
|
overrideAccess: true
|
|
5397
5468
|
});
|
|
5398
|
-
verifyLimiter.reset(
|
|
5469
|
+
await verifyLimiter.reset(limiterKey, req);
|
|
5399
5470
|
return Response.json({ success: true, verified: true });
|
|
5400
5471
|
}
|
|
5401
5472
|
return Response.json({ error: "Action invalide" }, { status: 400 });
|
|
@@ -5957,7 +6028,7 @@ function createImportConversationEndpoint(slugs, store) {
|
|
|
5957
6028
|
if (!isAuthed) {
|
|
5958
6029
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5959
6030
|
}
|
|
5960
|
-
const ip = req
|
|
6031
|
+
const ip = clientIpRateKey(req);
|
|
5961
6032
|
if (await importLimiter.check(ip, req)) {
|
|
5962
6033
|
return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
|
|
5963
6034
|
}
|
|
@@ -6165,6 +6236,21 @@ var WEBHOOK_URL_MESSAGES = {
|
|
|
6165
6236
|
scheme_not_allowed: "Seules les URL https:// sont accept\xE9es.",
|
|
6166
6237
|
private_host: "Les adresses priv\xE9es, loopback et link-local sont interdites (SSRF)."
|
|
6167
6238
|
};
|
|
6239
|
+
var MAX_PUSH_ENDPOINT_LENGTH = 2048;
|
|
6240
|
+
function validatePushEndpoint(raw) {
|
|
6241
|
+
if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_PUSH_ENDPOINT_LENGTH) {
|
|
6242
|
+
return { ok: false, reason: "invalid_url" };
|
|
6243
|
+
}
|
|
6244
|
+
let url;
|
|
6245
|
+
try {
|
|
6246
|
+
url = new URL(raw.trim());
|
|
6247
|
+
} catch {
|
|
6248
|
+
return { ok: false, reason: "invalid_url" };
|
|
6249
|
+
}
|
|
6250
|
+
if (url.protocol !== "https:") return { ok: false, reason: "scheme_not_allowed" };
|
|
6251
|
+
if (isBlockedHost(url.hostname)) return { ok: false, reason: "private_host" };
|
|
6252
|
+
return { ok: true, url };
|
|
6253
|
+
}
|
|
6168
6254
|
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
6169
6255
|
var LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("lookup-timed-out");
|
|
6170
6256
|
var NONEXISTENT_HOST_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA", "NOTFOUND"]);
|
|
@@ -6620,6 +6706,15 @@ async function sendPushToUser(payload, slugs, userId, notification) {
|
|
|
6620
6706
|
for (const s of subs.docs) {
|
|
6621
6707
|
const row = s;
|
|
6622
6708
|
if (!row.endpoint || !row.p256dh || !row.auth) continue;
|
|
6709
|
+
const check = validatePushEndpoint(row.endpoint);
|
|
6710
|
+
if (!check.ok || !check.url) {
|
|
6711
|
+
console.warn("[support] Push endpoint refused (unsafe URL):", check.reason);
|
|
6712
|
+
continue;
|
|
6713
|
+
}
|
|
6714
|
+
if (!await assertPublicHost(check.url.hostname)) {
|
|
6715
|
+
console.warn("[support] Push endpoint refused (host resolves to a private address)");
|
|
6716
|
+
continue;
|
|
6717
|
+
}
|
|
6623
6718
|
try {
|
|
6624
6719
|
await webpush.sendNotification(
|
|
6625
6720
|
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
|
@@ -6670,7 +6765,14 @@ function createPushSubscribeEndpoint(slugs) {
|
|
|
6670
6765
|
if (!endpoint || !p256dh || !auth) {
|
|
6671
6766
|
return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
|
|
6672
6767
|
}
|
|
6673
|
-
const
|
|
6768
|
+
const endpointCheck = validatePushEndpoint(endpoint);
|
|
6769
|
+
if (!endpointCheck.ok) {
|
|
6770
|
+
return Response.json(
|
|
6771
|
+
{ error: WEBHOOK_URL_MESSAGES[endpointCheck.reason || "invalid_url"] },
|
|
6772
|
+
{ status: 400 }
|
|
6773
|
+
);
|
|
6774
|
+
}
|
|
6775
|
+
const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: (req.headers.get("user-agent") || "").slice(0, 256) };
|
|
6674
6776
|
const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
|
|
6675
6777
|
if (existing.docs.length > 0) {
|
|
6676
6778
|
await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Thin typed wrappers around Payload data operations.
|
|
4
|
+
*
|
|
5
|
+
* Collection slugs are user-overridable config (`string`), not Payload's literal
|
|
6
|
+
* `CollectionSlug` union — so every call site otherwise needs `collection: X as any`.
|
|
7
|
+
* These helpers isolate that single unavoidable cast in ONE place, removing the
|
|
8
|
+
* ~200 scattered `as any` while staying an EXACT behavioral pass-through of options.
|
|
9
|
+
*
|
|
10
|
+
* Returns default to `any` (callers consume docs loosely, as before). Pass a type
|
|
11
|
+
* argument to opt into stronger typing: `dbFind<TicketData>(payload, slug, { ... })`.
|
|
12
|
+
*/
|
|
13
|
+
interface PaginatedResult<T> {
|
|
14
|
+
docs: T[];
|
|
15
|
+
totalDocs: number;
|
|
16
|
+
totalPages: number;
|
|
17
|
+
page?: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
hasNextPage: boolean;
|
|
20
|
+
hasPrevPage: boolean;
|
|
21
|
+
nextPage?: number | null;
|
|
22
|
+
prevPage?: number | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function dbFind<T = any>(payload: Payload, slug: string, options?: Record<string, unknown>): Promise<PaginatedResult<T>>;
|
|
25
|
+
export declare function dbFindByID<T = any>(payload: Payload, slug: string, options: {
|
|
26
|
+
id: number | string;
|
|
27
|
+
} & Record<string, unknown>): Promise<T>;
|
|
28
|
+
export declare function dbCreate<T = any>(payload: Payload, slug: string, options: Record<string, unknown>): Promise<T>;
|
|
29
|
+
export declare function dbUpdate<T = any>(payload: Payload, slug: string, options: Record<string, unknown>): Promise<T>;
|
|
30
|
+
export declare function dbCount(payload: Payload, slug: string, options?: Record<string, unknown>): Promise<{
|
|
31
|
+
totalDocs: number;
|
|
32
|
+
}>;
|
|
33
|
+
export declare function dbDelete<T = any>(payload: Payload, slug: string, options: Record<string, unknown>): Promise<T>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import { type TicketingFeatures } from './features.js';
|
|
3
|
+
export declare const SUPPORT_SETTINGS_PREF_KEY = "support-settings";
|
|
4
|
+
/**
|
|
5
|
+
* Key under which `plugin.ts` publishes the resolved staff collection slug on
|
|
6
|
+
* `config.custom`. It is the ONE source of truth shared by the read path (here)
|
|
7
|
+
* and the write path (`requireAdmin` → `slugs.users`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const SUPPORT_STAFF_SLUG_CONFIG_KEY = "supportStaffCollection";
|
|
10
|
+
/**
|
|
11
|
+
* Owner scope for every `payload-preferences` row this plugin reads back.
|
|
12
|
+
*
|
|
13
|
+
* `payload-preferences` is writable by ANY authenticated principal, whatever its
|
|
14
|
+
* auth collection: Payload's `POST /api/payload-preferences/:key` handler only
|
|
15
|
+
* checks `!!req.user` before upserting the row. Reading a plugin-wide row by key
|
|
16
|
+
* alone therefore lets a front-office user (or a support-client) plant their own
|
|
17
|
+
* row and have the whole plugin read it — replyTo addresses, SLA escalation
|
|
18
|
+
* address, AI provider, feature flags.
|
|
19
|
+
*
|
|
20
|
+
* Every read below is constrained to `user.relationTo = <staff collection>`, the
|
|
21
|
+
* same scope the WRITE path already uses (endpoints/settings.ts, signature.ts,
|
|
22
|
+
* user-prefs.ts all upsert with `req.user.collection`, and all three are guarded
|
|
23
|
+
* by `requireAdmin`, which compares against `slugs.users`).
|
|
24
|
+
*
|
|
25
|
+
* TWO SOURCES OF TRUTH WERE THE BUG: this used to resolve the scope from
|
|
26
|
+
* `config.admin.user`, which Payload defaults to the FIRST auth collection of
|
|
27
|
+
* the app when the integrator did not declare it (config/sanitize.js). On a host
|
|
28
|
+
* whose first auth collection is the front office and whose staff collection is
|
|
29
|
+
* declared through `collectionSlugs.users`, the read scope named the front
|
|
30
|
+
* office while the write scope named the staff — and the poisoning this scope
|
|
31
|
+
* was added to defeat was open again. So the plugin now PUBLISHES the resolved
|
|
32
|
+
* `slugs.users` on `config.custom` (see plugin.ts) and reads it back here.
|
|
33
|
+
* `config.admin.user` remains a last-resort fallback for callers using these
|
|
34
|
+
* helpers outside of a plugin-built config; there is no plugin deployment in
|
|
35
|
+
* which it is consulted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveStaffPrefSlug(payload: Payload, staffSlug?: string): string;
|
|
38
|
+
export interface SupportSettings {
|
|
39
|
+
email: {
|
|
40
|
+
fromAddress: string;
|
|
41
|
+
fromName: string;
|
|
42
|
+
replyToAddress: string;
|
|
43
|
+
};
|
|
44
|
+
ai: {
|
|
45
|
+
provider: string;
|
|
46
|
+
model: string;
|
|
47
|
+
enableSentiment: boolean;
|
|
48
|
+
enableSynthesis: boolean;
|
|
49
|
+
enableSuggestion: boolean;
|
|
50
|
+
enableRewrite: boolean;
|
|
51
|
+
};
|
|
52
|
+
sla: {
|
|
53
|
+
firstResponseMinutes: number;
|
|
54
|
+
resolutionMinutes: number;
|
|
55
|
+
businessHoursOnly: boolean;
|
|
56
|
+
escalationEmail: string;
|
|
57
|
+
};
|
|
58
|
+
autoClose: {
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
daysBeforeClose: number;
|
|
61
|
+
reminderDaysBefore: number;
|
|
62
|
+
};
|
|
63
|
+
/** Ticketing feature flags — server-authoritative since 2.1 (used to be per-browser localStorage). */
|
|
64
|
+
features: TicketingFeatures;
|
|
65
|
+
}
|
|
66
|
+
export interface UserPrefs {
|
|
67
|
+
locale: 'fr' | 'en';
|
|
68
|
+
signature: string;
|
|
69
|
+
}
|
|
70
|
+
export declare const DEFAULT_SETTINGS: SupportSettings;
|
|
71
|
+
export declare const DEFAULT_USER_PREFS: UserPrefs;
|
|
72
|
+
export interface SupportSettingsState {
|
|
73
|
+
settings: SupportSettings;
|
|
74
|
+
/**
|
|
75
|
+
* False when the preference row carries no `features` object yet — a
|
|
76
|
+
* pre-2.1 install, or a fresh one. Clients use it to decide whether their
|
|
77
|
+
* legacy `localStorage` flags should seed the server.
|
|
78
|
+
*/
|
|
79
|
+
featuresConfigured: boolean;
|
|
80
|
+
}
|
|
81
|
+
/** Invalidate the settings cache — call right after writing support settings. */
|
|
82
|
+
export declare function invalidateSupportSettingsCache(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Merge a stored preference value onto the defaults.
|
|
85
|
+
* Exported for the settings endpoint, which must not re-implement the merge.
|
|
86
|
+
*/
|
|
87
|
+
export declare function mergeSupportSettings(stored: Partial<SupportSettings> | undefined | null, base?: SupportSettings): SupportSettings;
|
|
88
|
+
export declare function readSupportSettingsState(payload: Payload, staffSlug?: string): Promise<SupportSettingsState>;
|
|
89
|
+
export declare function readSupportSettings(payload: Payload, staffSlug?: string): Promise<SupportSettings>;
|
|
90
|
+
export declare function readUserPrefs(payload: Payload, userId: string | number, staffSlug?: string): Promise<UserPrefs>;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { BillingClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const BillingView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const BillingView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "BillingView", children: /* @__PURE__ */ jsx(BillingClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { ChatViewClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const ChatView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const ChatView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "ChatView", children: /* @__PURE__ */ jsx(ChatViewClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { CrmClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const CrmView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const CrmView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "CrmView", children: /* @__PURE__ */ jsx(CrmClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { EmailTrackingClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const EmailTrackingView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const EmailTrackingView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "EmailTrackingView", children: /* @__PURE__ */ jsx(EmailTrackingClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { ImportConversationClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const ImportConversationView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const ImportConversationView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "ImportConversationView", children: /* @__PURE__ */ jsx(ImportConversationClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { LogsClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const LogsView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
10
12
|
return /* @__PURE__ */ jsx(
|
|
11
13
|
DefaultTemplate,
|
|
12
14
|
{
|
|
@@ -16,7 +18,7 @@ const LogsView = ({ initPageResult }) => {
|
|
|
16
18
|
payload: req.payload,
|
|
17
19
|
permissions: initPageResult.permissions,
|
|
18
20
|
searchParams: {},
|
|
19
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
20
22
|
visibleEntities,
|
|
21
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "LogsView", children: /* @__PURE__ */ jsx(LogsClient, {}) })
|
|
22
24
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { NewTicketClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const NewTicketView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
10
12
|
return /* @__PURE__ */ jsx(
|
|
11
13
|
DefaultTemplate,
|
|
12
14
|
{
|
|
@@ -16,7 +18,7 @@ const NewTicketView = ({ initPageResult }) => {
|
|
|
16
18
|
payload: req.payload,
|
|
17
19
|
permissions: initPageResult.permissions,
|
|
18
20
|
searchParams: {},
|
|
19
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
20
22
|
visibleEntities,
|
|
21
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "NewTicketView", children: /* @__PURE__ */ jsx(NewTicketClient, {}) })
|
|
22
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { PendingEmailsClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const PendingEmailsView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const PendingEmailsView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "PendingEmailsView", children: /* @__PURE__ */ jsx(PendingEmailsClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { SupportDashboardClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const SupportDashboardView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const SupportDashboardView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "SupportDashboardView", children: /* @__PURE__ */ jsx(SupportDashboardClient, {}) })
|
|
24
24
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
3
|
import { redirect } from 'next/navigation';
|
|
4
|
+
import { supportViewRedirectTarget } from '../shared/viewAccess.js';
|
|
4
5
|
import { AdminErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
5
6
|
import { TicketDetailClient } from './client.js';
|
|
6
7
|
|
|
7
8
|
const TicketDetailView = ({ initPageResult }) => {
|
|
8
9
|
const { req, visibleEntities } = initPageResult;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
const redirectTo = supportViewRedirectTarget(initPageResult);
|
|
11
|
+
if (redirectTo) redirect(redirectTo);
|
|
12
12
|
return /* @__PURE__ */ jsx(
|
|
13
13
|
DefaultTemplate,
|
|
14
14
|
{
|
|
@@ -18,7 +18,7 @@ const TicketDetailView = ({ initPageResult }) => {
|
|
|
18
18
|
payload: req.payload,
|
|
19
19
|
permissions: initPageResult.permissions,
|
|
20
20
|
searchParams: {},
|
|
21
|
-
user: req.user,
|
|
21
|
+
user: req.user ?? void 0,
|
|
22
22
|
visibleEntities,
|
|
23
23
|
children: /* @__PURE__ */ jsx(AdminErrorBoundary, { viewName: "TicketDetailView", children: /* @__PURE__ */ jsx(TicketDetailClient, {}) })
|
|
24
24
|
}
|