@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.
Files changed (49) hide show
  1. package/README.md +24 -14
  2. package/dist/index.cjs +115 -13
  3. package/dist/index.d.cts +20 -0
  4. package/dist/index.d.ts +20 -0
  5. package/dist/index.js +115 -13
  6. package/dist/utils/db.d.ts +34 -0
  7. package/dist/utils/readSettings.d.ts +90 -0
  8. package/dist/views/BillingView/index.js +4 -4
  9. package/dist/views/ChatView/index.js +4 -4
  10. package/dist/views/CrmView/index.js +4 -4
  11. package/dist/views/EmailTrackingView/index.js +4 -4
  12. package/dist/views/ImportConversationView/index.js +4 -4
  13. package/dist/views/LogsView/index.js +4 -2
  14. package/dist/views/NewTicketView/index.js +4 -2
  15. package/dist/views/PendingEmailsView/index.js +4 -4
  16. package/dist/views/SupportDashboardView/index.js +4 -4
  17. package/dist/views/TicketDetailView/index.js +4 -4
  18. package/dist/views/TicketInboxView/index.js +4 -2
  19. package/dist/views/TicketingSettingsView/index.js +4 -4
  20. package/dist/views/TimeDashboardView/index.js +4 -4
  21. package/dist/views/shared/viewAccess.d.ts +29 -0
  22. package/dist/views/shared/viewAccess.js +24 -0
  23. package/package.json +26 -20
  24. package/src/endpoints/auth-2fa.ts +53 -8
  25. package/src/endpoints/capabilities.ts +2 -4
  26. package/src/endpoints/chatbot.ts +2 -2
  27. package/src/endpoints/import-conversation.ts +2 -2
  28. package/src/endpoints/login.ts +13 -3
  29. package/src/endpoints/push.ts +14 -1
  30. package/src/endpoints/statuses.ts +17 -0
  31. package/src/portal/login/page.tsx +14 -4
  32. package/src/utils/push.ts +22 -0
  33. package/src/utils/rateLimiter.ts +98 -0
  34. package/src/utils/twoFactorChallenge.ts +6 -1
  35. package/src/utils/urlSafety.ts +36 -1
  36. package/src/views/BillingView/index.tsx +4 -4
  37. package/src/views/ChatView/index.tsx +4 -4
  38. package/src/views/CrmView/index.tsx +4 -4
  39. package/src/views/EmailTrackingView/index.tsx +4 -4
  40. package/src/views/ImportConversationView/index.tsx +4 -4
  41. package/src/views/LogsView/index.tsx +4 -2
  42. package/src/views/NewTicketView/index.tsx +4 -2
  43. package/src/views/PendingEmailsView/index.tsx +4 -4
  44. package/src/views/SupportDashboardView/index.tsx +4 -4
  45. package/src/views/TicketDetailView/index.tsx +4 -4
  46. package/src/views/TicketInboxView/index.tsx +4 -2
  47. package/src/views/TicketingSettingsView/index.tsx +4 -4
  48. package/src/views/TimeDashboardView/index.tsx +4 -4
  49. 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 = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
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 = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
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 = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
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 genericSendResponse = { success: true, message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9." };
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(email, req)) {
5317
- return Response.json(genericSendResponse);
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 Response.json(genericSendResponse);
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 Response.json(genericSendResponse);
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(email, req)) {
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(email);
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.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
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 data = { user: req.user.id, endpoint, p256dh, auth, userAgent: req.headers.get("user-agent") || "" };
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) redirect("/admin/login");
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
- if (!req.user) redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
- if (!req.user) {
10
- redirect("/admin/login");
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
  }