@consilioweb/payload-support 3.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 (77) hide show
  1. package/README.md +58 -14
  2. package/dist/index.cjs +673 -107
  3. package/dist/index.d.cts +40 -5
  4. package/dist/index.d.ts +40 -5
  5. package/dist/index.js +673 -107
  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/collections/ChatMessages.ts +59 -2
  25. package/src/collections/ClientSummaries.ts +10 -4
  26. package/src/collections/TicketMessages.ts +4 -1
  27. package/src/collections/WebhookEndpoints.ts +44 -2
  28. package/src/endpoints/admin-chat.ts +3 -3
  29. package/src/endpoints/ai-agent.ts +3 -3
  30. package/src/endpoints/ai.ts +3 -3
  31. package/src/endpoints/auth-2fa.ts +68 -11
  32. package/src/endpoints/capabilities.ts +7 -9
  33. package/src/endpoints/chat.ts +7 -5
  34. package/src/endpoints/chatbot.ts +50 -4
  35. package/src/endpoints/client-intelligence.ts +4 -4
  36. package/src/endpoints/email-stats.ts +19 -3
  37. package/src/endpoints/import-conversation.ts +3 -3
  38. package/src/endpoints/index.ts +1 -1
  39. package/src/endpoints/invite-collaborator.ts +29 -3
  40. package/src/endpoints/login.ts +28 -5
  41. package/src/endpoints/oauth-google.ts +130 -8
  42. package/src/endpoints/push.ts +14 -1
  43. package/src/endpoints/resend-notification.ts +3 -3
  44. package/src/endpoints/send-reminder.ts +3 -3
  45. package/src/endpoints/signature.ts +9 -2
  46. package/src/endpoints/statuses.ts +17 -0
  47. package/src/endpoints/ticket-synthesis.ts +3 -3
  48. package/src/endpoints/transfer-ticket.ts +28 -3
  49. package/src/endpoints/typing.ts +117 -14
  50. package/src/endpoints/user-prefs.ts +5 -2
  51. package/src/plugin.ts +12 -0
  52. package/src/portal/auth/layout.tsx +19 -1
  53. package/src/portal/auth/tickets/detail/MessageBody.tsx +88 -0
  54. package/src/portal/auth/tickets/detail/page.tsx +2 -6
  55. package/src/portal/login/page.tsx +23 -5
  56. package/src/utils/fireWebhooks.ts +4 -1
  57. package/src/utils/push.ts +22 -0
  58. package/src/utils/rateLimiter.ts +136 -4
  59. package/src/utils/readSettings.ts +124 -14
  60. package/src/utils/ticketAccess.ts +16 -1
  61. package/src/utils/twoFactorChallenge.ts +85 -0
  62. package/src/utils/urlSafety.ts +265 -0
  63. package/src/utils/webhookDispatcher.ts +5 -1
  64. package/src/views/BillingView/index.tsx +4 -4
  65. package/src/views/ChatView/index.tsx +4 -4
  66. package/src/views/CrmView/index.tsx +4 -4
  67. package/src/views/EmailTrackingView/index.tsx +4 -4
  68. package/src/views/ImportConversationView/index.tsx +4 -4
  69. package/src/views/LogsView/index.tsx +4 -2
  70. package/src/views/NewTicketView/index.tsx +4 -2
  71. package/src/views/PendingEmailsView/index.tsx +4 -4
  72. package/src/views/SupportDashboardView/index.tsx +4 -4
  73. package/src/views/TicketDetailView/index.tsx +4 -4
  74. package/src/views/TicketInboxView/index.tsx +4 -2
  75. package/src/views/TicketingSettingsView/index.tsx +4 -4
  76. package/src/views/TimeDashboardView/index.tsx +4 -4
  77. package/src/views/shared/viewAccess.ts +73 -0
@@ -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
  }
@@ -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 { TicketInboxClient } from './client.js';
6
7
 
7
8
  const TicketInboxView = ({ 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 TicketInboxView = ({ 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: "TicketInboxView", children: /* @__PURE__ */ jsx(TicketInboxClient, {}) })
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 { TicketingSettingsClient } from './client.js';
6
7
 
7
8
  const TicketingSettingsView = ({ 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 TicketingSettingsView = ({ 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: "TicketingSettingsView", children: /* @__PURE__ */ jsx(TicketingSettingsClient, {}) })
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 { TimeDashboardClient } from './client.js';
6
7
 
7
8
  const TimeDashboardView = ({ 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 TimeDashboardView = ({ 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: "TimeDashboardView", children: /* @__PURE__ */ jsx(TimeDashboardClient, {}) })
24
24
  }
@@ -0,0 +1,29 @@
1
+ import type { AdminViewServerProps } from 'payload';
2
+ /**
3
+ * Where a custom admin view must send a caller it refuses, or `null` to render.
4
+ *
5
+ * Payload does NOT gate custom admin views. `RootPage` skips its own
6
+ * `canAccessAdmin` redirect as soon as `isCustomAdminView()` matches, and that
7
+ * helper only compares the request path against the registered `view.path` —
8
+ * it reads no visibility flag, despite what its docblock claims. Authorising a
9
+ * custom view is therefore the view's own job, and every view registered by
10
+ * this plugin sits at a custom path (`/support/inbox`, `/support/ticket`, …).
11
+ *
12
+ * `!req.user` alone is not that check. A single `payload-token` cookie serves
13
+ * every auth collection of the host app, so an ordinary front-office account —
14
+ * a `customers`, `members` or `subscribers` signup — carries one on `/admin`
15
+ * routes too. Such a caller reached `DefaultTemplate` and received the admin
16
+ * chrome together with the client config Payload builds for any authenticated
17
+ * request: the field schema of every collection and global, `admin.hidden`
18
+ * ones included, plus an unfiltered `visibleEntities`.
19
+ *
20
+ * The ticket data itself never travelled — the client components fetch through
21
+ * `/api/support/*`, which `requireAdmin` has guarded all along — but the shape
22
+ * of the whole CMS did.
23
+ *
24
+ * The gate mirrors `requireAdmin` (utils/auth.ts): membership of the staff
25
+ * collection. It additionally honours `canAccessAdmin`, so an account the host
26
+ * disabled through its own `access.admin` is refused here too, and it fails
27
+ * closed on anything it cannot resolve.
28
+ */
29
+ export declare function supportViewRedirectTarget(initPageResult: AdminViewServerProps['initPageResult']): string | null;
@@ -0,0 +1,24 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ import { SUPPORT_STAFF_SLUG_CONFIG_KEY } from '../../utils/readSettings.js';
3
+
4
+ function supportViewRedirectTarget(initPageResult) {
5
+ const req = initPageResult?.req;
6
+ const config = req?.payload?.config;
7
+ const adminRoute = config?.routes?.admin ?? "/admin";
8
+ const routes = config?.admin?.routes;
9
+ const to = (route, fallback) => formatAdminURL({ adminRoute, path: route ?? fallback });
10
+ if (!req?.user) return to(routes?.login, "/login");
11
+ if (initPageResult?.permissions?.canAccessAdmin === false) {
12
+ return to(routes?.unauthorized, "/unauthorized");
13
+ }
14
+ const custom = config?.custom;
15
+ const registered = custom?.[SUPPORT_STAFF_SLUG_CONFIG_KEY];
16
+ const staffSlug = typeof registered === "string" && registered || config?.admin?.user || null;
17
+ const collection = req.user.collection;
18
+ if (!staffSlug || !collection || collection !== staffSlug) {
19
+ return to(routes?.unauthorized, "/unauthorized");
20
+ }
21
+ return null;
22
+ }
23
+
24
+ export { supportViewRedirectTarget };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "3.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -85,12 +85,13 @@
85
85
  "author": "ConsilioWEB <contact@consilioweb.fr> (https://consilioweb.fr)",
86
86
  "license": "MIT",
87
87
  "peerDependencies": {
88
- "@payloadcms/next": "^3.37.0",
88
+ "@payloadcms/next": "^3.79.1",
89
89
  "lucide-react": ">=0.300.0",
90
90
  "next": "^15.2.9 || ^16.0.0",
91
- "payload": "^3.37.0",
91
+ "payload": "^3.79.1",
92
92
  "react": "^19.0.0",
93
- "react-dom": "^19.0.0"
93
+ "react-dom": "^19.0.0",
94
+ "@payloadcms/richtext-lexical": "^3.79.1"
94
95
  },
95
96
  "engines": {
96
97
  "node": ">=20.9.0"
@@ -100,32 +101,37 @@
100
101
  "provenance": true
101
102
  },
102
103
  "devDependencies": {
103
- "@payloadcms/db-sqlite": "^3.86.0",
104
- "@payloadcms/graphql": "^3.86.0",
105
- "@payloadcms/next": "^3.86.0",
106
- "@payloadcms/richtext-lexical": "^3.86.0",
107
- "@payloadcms/translations": "^3.86.0",
108
- "@payloadcms/ui": "^3.86.0",
109
- "@playwright/test": "^1.49.0",
104
+ "@payloadcms/db-sqlite": "^3.88.0",
105
+ "@payloadcms/graphql": "^3.88.0",
106
+ "@payloadcms/next": "^3.88.0",
107
+ "@payloadcms/richtext-lexical": "^3.88.0",
108
+ "@payloadcms/translations": "^3.88.0",
109
+ "@payloadcms/ui": "^3.88.0",
110
+ "@playwright/test": "^1.63.0",
110
111
  "@types/pdfkit": "^0.17.6",
111
- "@types/react": "^19.0.0",
112
+ "@types/react": "^19.2.18",
112
113
  "@types/sanitize-html": "^2.16.1",
113
114
  "@types/web-push": "^3.6.4",
114
115
  "esbuild-sass-plugin": "^3.7.0",
115
- "next": "^16.2.9",
116
- "payload": "^3.86.0",
117
- "react": "^19.2.7",
118
- "react-dom": "^19.2.7",
116
+ "next": "^16.3.4",
117
+ "payload": "^3.88.0",
118
+ "react": "^19.2.8",
119
+ "react-dom": "^19.2.8",
119
120
  "sharp": "0.34.5",
120
- "tsup": "^8.0.0",
121
- "typescript": "^5.5.0",
122
- "vitest": "^4.1.9"
121
+ "tsup": "^8.5.1",
122
+ "typescript": "^5.9.3",
123
+ "vitest": "^4.1.11"
123
124
  },
124
125
  "dependencies": {
125
126
  "pdfkit": "^0.19.1",
126
- "sanitize-html": "^2.17.5",
127
+ "sanitize-html": "^2.17.7",
127
128
  "web-push": "^3.6.7"
128
129
  },
130
+ "peerDependenciesMeta": {
131
+ "@payloadcms/richtext-lexical": {
132
+ "optional": true
133
+ }
134
+ },
129
135
  "scripts": {
130
136
  "build": "tsup && tsc -p tsconfig.types.json && node scripts/copy-subpath-types.mjs",
131
137
  "typecheck": "tsc --noEmit",