@consilioweb/payload-support 1.1.0 → 2.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 +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +22 -14
- package/dist/components/TicketConversation/components/AISummaryPanel.js +23 -16
- package/dist/components/TicketConversation/components/ActionPanels.cjs +5 -5
- package/dist/components/TicketConversation/components/ActionPanels.js +5 -5
- package/dist/components/TicketConversation/components/ClientBar.cjs +1 -1
- package/dist/components/TicketConversation/components/ClientBar.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.cjs +2 -2
- package/dist/components/TicketConversation/components/QuickActions.js +2 -2
- package/dist/components/TicketConversation/components/TicketHeader.cjs +1 -1
- package/dist/components/TicketConversation/components/TicketHeader.js +1 -1
- package/dist/components/TicketConversation/index.cjs +10 -10
- package/dist/components/TicketConversation/index.js +10 -10
- package/dist/index.cjs +710 -174
- package/dist/index.d.cts +165 -3
- package/dist/index.d.ts +165 -3
- package/dist/index.js +703 -176
- package/dist/styles/ChatView.module.scss +1 -1
- package/dist/styles/TicketDetail.module.scss +3 -3
- package/dist/styles/_tokens.scss +1 -1
- package/dist/styles/theme.css +6 -6
- package/dist/views/TicketingSettingsView/client.cjs +3 -3
- package/dist/views/TicketingSettingsView/client.js +3 -3
- package/package.json +9 -9
- package/src/__tests__/aiSummaryRendering.test.ts +21 -0
- package/src/__tests__/authResponses.test.ts +69 -0
- package/src/__tests__/capabilities.test.ts +71 -0
- package/src/__tests__/generateTrackingToken.test.ts +18 -4
- package/src/__tests__/integration/core-behaviors.test.ts +23 -0
- package/src/__tests__/rateLimiter.test.ts +60 -66
- package/src/__tests__/trackOpenEndpoint.test.ts +54 -0
- package/src/__tests__/webhookSecurity.test.ts +58 -0
- package/src/collections/PendingEmails.ts +2 -1
- package/src/collections/SupportClients.ts +43 -1
- package/src/collections/SupportCounters.ts +14 -0
- package/src/collections/SupportRateLimits.ts +20 -0
- package/src/collections/TicketMessages.ts +76 -0
- package/src/collections/Tickets.ts +183 -40
- package/src/collections/index.ts +2 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +26 -13
- package/src/components/TicketConversation/components/ActionPanels.tsx +5 -5
- package/src/components/TicketConversation/components/ClientBar.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +2 -2
- package/src/components/TicketConversation/components/TicketHeader.tsx +1 -1
- package/src/components/TicketConversation/index.tsx +10 -10
- package/src/endpoints/admin-chat.ts +4 -4
- package/src/endpoints/ai-agent.ts +6 -1
- package/src/endpoints/ai.ts +10 -5
- package/src/endpoints/auth-2fa.ts +6 -7
- package/src/endpoints/auto-close.ts +2 -1
- package/src/endpoints/capabilities.ts +144 -0
- package/src/endpoints/channels.ts +2 -1
- package/src/endpoints/chat.ts +6 -6
- package/src/endpoints/chatbot.ts +4 -4
- package/src/endpoints/client-intelligence.ts +13 -4
- package/src/endpoints/import-conversation.ts +6 -5
- package/src/endpoints/index.ts +35 -14
- package/src/endpoints/invite-collaborator.ts +4 -4
- package/src/endpoints/login.ts +4 -5
- package/src/endpoints/oauth-google.ts +1 -1
- package/src/endpoints/process-digests.ts +2 -1
- package/src/endpoints/process-scheduled.ts +2 -1
- package/src/endpoints/process-snooze.ts +2 -1
- package/src/endpoints/resend-notification.ts +4 -4
- package/src/endpoints/send-reminder.ts +4 -4
- package/src/endpoints/ticket-synthesis.ts +17 -2
- package/src/endpoints/track-open.ts +44 -21
- package/src/endpoints/transfer-ticket.ts +4 -4
- package/src/index.ts +7 -0
- package/src/plugin.ts +16 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/ChatWidget.tsx +4 -4
- package/src/portal/auth/ChatbotWidget.tsx +4 -4
- package/src/portal/auth/dashboard/DashboardClient.tsx +2 -2
- package/src/portal/auth/faq/FAQSearch.tsx +1 -1
- package/src/portal/auth/faq/page.tsx +2 -2
- package/src/portal/auth/profile/page.tsx +17 -17
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +3 -3
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +3 -3
- package/src/portal/auth/tickets/detail/page.tsx +5 -5
- package/src/portal/auth/tickets/new/page.tsx +8 -8
- package/src/portal/forgot-password/page.tsx +3 -3
- package/src/portal/layout.tsx +5 -2
- package/src/portal/login/page.tsx +7 -7
- package/src/portal/page.tsx +9 -9
- package/src/portal/register/page.tsx +4 -4
- package/src/portal/reset-password/page.tsx +3 -3
- package/src/styles/ChatView.module.scss +1 -1
- package/src/styles/TicketDetail.module.scss +3 -3
- package/src/styles/_tokens.scss +1 -1
- package/src/styles/theme.css +6 -6
- package/src/types.ts +71 -0
- package/src/utils/rateLimiter.ts +131 -38
- package/src/utils/slugs.ts +4 -0
- package/src/utils/webhookSecurity.ts +81 -0
- package/src/views/TicketingSettingsView/client.tsx +3 -3
package/src/portal/layout.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
+
import { headers } from 'next/headers'
|
|
2
3
|
|
|
3
4
|
export const metadata = {
|
|
4
5
|
title: 'Support Client',
|
|
@@ -6,19 +7,21 @@ export const metadata = {
|
|
|
6
7
|
robots: { index: false, follow: false },
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export default function SupportRootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
export default async function SupportRootLayout({ children }: { children: React.ReactNode }) {
|
|
11
|
+
const nonce = (await headers()).get('x-nonce') || undefined
|
|
10
12
|
return (
|
|
11
13
|
<html lang="fr" suppressHydrationWarning>
|
|
12
14
|
<head>
|
|
13
15
|
<link href="/favicon.ico" rel="icon" sizes="32x32" />
|
|
14
16
|
<meta name="theme-color" content="#2563eb" />
|
|
15
17
|
<script
|
|
18
|
+
nonce={nonce}
|
|
16
19
|
dangerouslySetInnerHTML={{
|
|
17
20
|
__html: `(function(){try{if(localStorage.getItem('support-dark-mode')==='true'){document.documentElement.setAttribute('data-theme','dark')}}catch(e){console.error('[support-layout] Failed to get user:',e)}})()`,
|
|
18
21
|
}}
|
|
19
22
|
/>
|
|
20
23
|
</head>
|
|
21
|
-
<body className="min-h-
|
|
24
|
+
<body className="min-h-[100dvh] bg-gray-50 dark:bg-gray-950" suppressHydrationWarning>
|
|
22
25
|
{children}
|
|
23
26
|
</body>
|
|
24
27
|
</html>
|
|
@@ -145,7 +145,7 @@ function SupportLoginContent() {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
return (
|
|
148
|
-
<div className="flex min-h-
|
|
148
|
+
<div className="flex min-h-[100dvh] flex-col items-center justify-center px-4 py-12 bg-gradient-to-br from-slate-50 via-white to-blue-50 dark:from-slate-950 dark:via-slate-950 dark:to-blue-950/20">
|
|
149
149
|
{/* Decorative background element */}
|
|
150
150
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,rgba(37,99,235,0.08),transparent)] dark:bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,rgba(37,99,235,0.12),transparent)]" />
|
|
151
151
|
|
|
@@ -208,7 +208,7 @@ function SupportLoginContent() {
|
|
|
208
208
|
value={twoFactorCode}
|
|
209
209
|
onChange={(e) => setTwoFactorCode(e.target.value.replace(/\D/g, ''))}
|
|
210
210
|
placeholder="000000"
|
|
211
|
-
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-center text-2xl font-semibold tracking-[0.4em] text-slate-900 outline-none transition-
|
|
211
|
+
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-center text-2xl font-semibold tracking-[0.4em] text-slate-900 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none placeholder:text-slate-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 dark:border-slate-600 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-600 dark:focus:border-blue-500 dark:focus:ring-blue-500/20"
|
|
212
212
|
autoFocus
|
|
213
213
|
/>
|
|
214
214
|
</div>
|
|
@@ -216,7 +216,7 @@ function SupportLoginContent() {
|
|
|
216
216
|
<button
|
|
217
217
|
type="submit"
|
|
218
218
|
disabled={loading || twoFactorCode.length !== 6}
|
|
219
|
-
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-
|
|
219
|
+
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 disabled:shadow-none"
|
|
220
220
|
>
|
|
221
221
|
{loading ? 'Verification...' : 'Verifier'}
|
|
222
222
|
</button>
|
|
@@ -274,7 +274,7 @@ function SupportLoginContent() {
|
|
|
274
274
|
value={email}
|
|
275
275
|
onChange={(e) => setEmail(e.target.value)}
|
|
276
276
|
placeholder="vous@entreprise.fr"
|
|
277
|
-
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-
|
|
277
|
+
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none placeholder:text-slate-400 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 dark:border-slate-600 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 dark:focus:border-blue-500 dark:focus:ring-blue-500/20"
|
|
278
278
|
/>
|
|
279
279
|
</div>
|
|
280
280
|
|
|
@@ -289,14 +289,14 @@ function SupportLoginContent() {
|
|
|
289
289
|
value={password}
|
|
290
290
|
onChange={(e) => setPassword(e.target.value)}
|
|
291
291
|
placeholder="••••••••"
|
|
292
|
-
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-
|
|
292
|
+
className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none placeholder:text-slate-400 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 dark:border-slate-600 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 dark:focus:border-blue-500 dark:focus:ring-blue-500/20"
|
|
293
293
|
/>
|
|
294
294
|
</div>
|
|
295
295
|
|
|
296
296
|
<button
|
|
297
297
|
type="submit"
|
|
298
298
|
disabled={loading || sending2FA}
|
|
299
|
-
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-
|
|
299
|
+
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 disabled:shadow-none"
|
|
300
300
|
>
|
|
301
301
|
{loading || sending2FA ? 'Connexion...' : 'Se connecter'}
|
|
302
302
|
</button>
|
|
@@ -312,7 +312,7 @@ function SupportLoginContent() {
|
|
|
312
312
|
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
|
|
313
313
|
<a
|
|
314
314
|
href="/api/support/oauth/google?action=login"
|
|
315
|
-
className="flex w-full items-center justify-center gap-3 rounded-xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition-
|
|
315
|
+
className="flex w-full items-center justify-center gap-3 rounded-xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-slate-50 hover:shadow-md active:scale-[0.98] dark:border-slate-600 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
|
|
316
316
|
>
|
|
317
317
|
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
318
318
|
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
|
package/src/portal/page.tsx
CHANGED
|
@@ -6,7 +6,7 @@ const supportEmail = process.env.SUPPORT_EMAIL || 'support@example.com'
|
|
|
6
6
|
|
|
7
7
|
export default function SupportPage() {
|
|
8
8
|
return (
|
|
9
|
-
<div className="flex min-h-
|
|
9
|
+
<div className="flex min-h-[100dvh] flex-col bg-white dark:bg-slate-950">
|
|
10
10
|
{/* Header */}
|
|
11
11
|
<header className="border-b border-slate-200 bg-white/80 backdrop-blur-lg dark:border-slate-800 dark:bg-slate-950/80">
|
|
12
12
|
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
|
|
@@ -20,7 +20,7 @@ export default function SupportPage() {
|
|
|
20
20
|
</Link>
|
|
21
21
|
<Link
|
|
22
22
|
href="/support/login"
|
|
23
|
-
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-medium text-white shadow-sm transition-
|
|
23
|
+
className="rounded-lg bg-blue-600 px-5 py-2 text-sm font-medium text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
|
|
24
24
|
>
|
|
25
25
|
Se connecter
|
|
26
26
|
</Link>
|
|
@@ -49,7 +49,7 @@ export default function SupportPage() {
|
|
|
49
49
|
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
50
50
|
<Link
|
|
51
51
|
href="/support/login"
|
|
52
|
-
className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-8 py-3.5 text-base font-medium text-white shadow-lg shadow-blue-600/25 transition-
|
|
52
|
+
className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-8 py-3.5 text-base font-medium text-white shadow-lg shadow-blue-600/25 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl hover:shadow-blue-600/30 active:scale-[0.98] dark:shadow-blue-600/20"
|
|
53
53
|
>
|
|
54
54
|
Accéder à mon espace
|
|
55
55
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
|
|
@@ -58,7 +58,7 @@ export default function SupportPage() {
|
|
|
58
58
|
</Link>
|
|
59
59
|
<Link
|
|
60
60
|
href="/support/register"
|
|
61
|
-
className="inline-flex items-center rounded-xl border border-slate-300 bg-white px-8 py-3.5 text-base font-medium text-slate-700 shadow-sm transition-
|
|
61
|
+
className="inline-flex items-center rounded-xl border border-slate-300 bg-white px-8 py-3.5 text-base font-medium text-slate-700 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-slate-50 hover:shadow-md active:scale-[0.98] dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
|
62
62
|
>
|
|
63
63
|
Créer un compte
|
|
64
64
|
</Link>
|
|
@@ -80,7 +80,7 @@ export default function SupportPage() {
|
|
|
80
80
|
</p>
|
|
81
81
|
<div className="grid gap-8 sm:grid-cols-3">
|
|
82
82
|
{/* Card 1 */}
|
|
83
|
-
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-
|
|
83
|
+
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:shadow-lg hover:-translate-y-0.5 dark:border-slate-700 dark:bg-slate-800">
|
|
84
84
|
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 text-blue-600 transition-colors group-hover:bg-blue-600 group-hover:text-white dark:bg-blue-950 dark:text-blue-400 dark:group-hover:bg-blue-600 dark:group-hover:text-white">
|
|
85
85
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6">
|
|
86
86
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
@@ -96,7 +96,7 @@ export default function SupportPage() {
|
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
98
|
{/* Card 2 */}
|
|
99
|
-
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-
|
|
99
|
+
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:shadow-lg hover:-translate-y-0.5 dark:border-slate-700 dark:bg-slate-800">
|
|
100
100
|
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 text-blue-600 transition-colors group-hover:bg-blue-600 group-hover:text-white dark:bg-blue-950 dark:text-blue-400 dark:group-hover:bg-blue-600 dark:group-hover:text-white">
|
|
101
101
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6">
|
|
102
102
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
@@ -109,7 +109,7 @@ export default function SupportPage() {
|
|
|
109
109
|
</div>
|
|
110
110
|
|
|
111
111
|
{/* Card 3 */}
|
|
112
|
-
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-
|
|
112
|
+
<div className="group rounded-2xl border border-slate-200 bg-white p-8 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:shadow-lg hover:-translate-y-0.5 dark:border-slate-700 dark:bg-slate-800">
|
|
113
113
|
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100 text-blue-600 transition-colors group-hover:bg-blue-600 group-hover:text-white dark:bg-blue-950 dark:text-blue-400 dark:group-hover:bg-blue-600 dark:group-hover:text-white">
|
|
114
114
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6">
|
|
115
115
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -139,13 +139,13 @@ export default function SupportPage() {
|
|
|
139
139
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
140
140
|
<Link
|
|
141
141
|
href="/support/register"
|
|
142
|
-
className="inline-flex items-center rounded-xl bg-blue-600 px-8 py-3.5 text-base font-medium text-white shadow-lg shadow-blue-600/25 transition-
|
|
142
|
+
className="inline-flex items-center rounded-xl bg-blue-600 px-8 py-3.5 text-base font-medium text-white shadow-lg shadow-blue-600/25 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl active:scale-[0.98]"
|
|
143
143
|
>
|
|
144
144
|
Créer un compte
|
|
145
145
|
</Link>
|
|
146
146
|
<a
|
|
147
147
|
href={`mailto:${supportEmail}`}
|
|
148
|
-
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-8 py-3.5 text-base font-medium text-slate-700 shadow-sm transition-
|
|
148
|
+
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-8 py-3.5 text-base font-medium text-slate-700 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-slate-50 hover:shadow-md active:scale-[0.98] dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
|
149
149
|
>
|
|
150
150
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5 text-slate-400">
|
|
151
151
|
<path d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
|
@@ -73,10 +73,10 @@ export default function SupportRegisterPage() {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const inputClassName =
|
|
76
|
-
'w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-
|
|
76
|
+
'w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none placeholder:text-slate-400 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10 dark:border-slate-600 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500 dark:focus:border-blue-500 dark:focus:ring-blue-500/20'
|
|
77
77
|
|
|
78
78
|
return (
|
|
79
|
-
<div className="flex min-h-
|
|
79
|
+
<div className="flex min-h-[100dvh] flex-col items-center justify-center px-4 py-12 bg-gradient-to-br from-slate-50 via-white to-blue-50 dark:from-slate-950 dark:via-slate-950 dark:to-blue-950/20">
|
|
80
80
|
{/* Decorative background element */}
|
|
81
81
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,rgba(37,99,235,0.08),transparent)] dark:bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,rgba(37,99,235,0.12),transparent)]" />
|
|
82
82
|
|
|
@@ -242,7 +242,7 @@ export default function SupportRegisterPage() {
|
|
|
242
242
|
<button
|
|
243
243
|
type="submit"
|
|
244
244
|
disabled={loading}
|
|
245
|
-
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-
|
|
245
|
+
className="w-full rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 disabled:shadow-none"
|
|
246
246
|
>
|
|
247
247
|
{loading ? 'Creation du compte...' : 'Créer mon compte'}
|
|
248
248
|
</button>
|
|
@@ -257,7 +257,7 @@ export default function SupportRegisterPage() {
|
|
|
257
257
|
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
|
|
258
258
|
<a
|
|
259
259
|
href="/api/support/oauth/google?action=register"
|
|
260
|
-
className="flex w-full items-center justify-center gap-3 rounded-xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition-
|
|
260
|
+
className="flex w-full items-center justify-center gap-3 rounded-xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-slate-50 hover:shadow-md active:scale-[0.98] dark:border-slate-600 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
|
|
261
261
|
>
|
|
262
262
|
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
263
263
|
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
|
|
@@ -82,7 +82,7 @@ function ResetPasswordForm() {
|
|
|
82
82
|
</div>
|
|
83
83
|
<Link
|
|
84
84
|
href="/support/login"
|
|
85
|
-
className="block w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-center text-base font-black text-black shadow-[4px_4px_0px_#000] transition-
|
|
85
|
+
className="block w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-center text-base font-black text-black shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
|
|
86
86
|
>
|
|
87
87
|
Se connecter
|
|
88
88
|
</Link>
|
|
@@ -131,7 +131,7 @@ function ResetPasswordForm() {
|
|
|
131
131
|
<button
|
|
132
132
|
type="submit"
|
|
133
133
|
disabled={loading}
|
|
134
|
-
className="w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-base font-black text-black shadow-[4px_4px_0px_#000] transition-
|
|
134
|
+
className="w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-base font-black text-black shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] disabled:opacity-50"
|
|
135
135
|
>
|
|
136
136
|
{loading ? 'Modification...' : 'Modifier le mot de passe'}
|
|
137
137
|
</button>
|
|
@@ -143,7 +143,7 @@ function ResetPasswordForm() {
|
|
|
143
143
|
|
|
144
144
|
export default function ResetPasswordPage() {
|
|
145
145
|
return (
|
|
146
|
-
<div className="flex min-h-
|
|
146
|
+
<div className="flex min-h-[100dvh] items-center justify-center px-4 py-12">
|
|
147
147
|
<Suspense fallback={<p className="text-gray-400">Chargement...</p>}>
|
|
148
148
|
<ResetPasswordForm />
|
|
149
149
|
</Suspense>
|
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
gap: 8px;
|
|
532
532
|
padding: 8px 14px;
|
|
533
533
|
font-size: 12px;
|
|
534
|
-
color: #
|
|
534
|
+
color: #17807c;
|
|
535
535
|
font-weight: 500;
|
|
536
536
|
}
|
|
537
537
|
|
|
@@ -539,7 +539,7 @@
|
|
|
539
539
|
display: flex;
|
|
540
540
|
gap: 3px;
|
|
541
541
|
span {
|
|
542
|
-
width: 5px; height: 5px; border-radius: 50%; background: #
|
|
542
|
+
width: 5px; height: 5px; border-radius: 50%; background: #17807c;
|
|
543
543
|
animation: bounce 1.2s infinite;
|
|
544
544
|
&:nth-child(2) { animation-delay: 150ms; }
|
|
545
545
|
&:nth-child(3) { animation-delay: 300ms; }
|
|
@@ -612,7 +612,7 @@
|
|
|
612
612
|
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
-
//
|
|
615
|
+
// AI button — teal accent, pill style
|
|
616
616
|
.toolbarBtnAi {
|
|
617
617
|
background: var(--theme-accent-50, color-mix(in srgb, var(--theme-accent-500, #6b46e8) 10%, transparent));
|
|
618
618
|
border-color: color-mix(in srgb, var(--theme-accent-500, #6b46e8) 25%, transparent);
|
package/src/styles/_tokens.scss
CHANGED
package/src/styles/theme.css
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
--support-cat-core: #2563eb;
|
|
137
137
|
--support-cat-communication: #16a34a;
|
|
138
138
|
--support-cat-productivity: #d97706;
|
|
139
|
-
--support-cat-advanced: #
|
|
139
|
+
--support-cat-advanced: #17807c;
|
|
140
140
|
|
|
141
141
|
/* ------------------------------------------
|
|
142
142
|
* 8. PROJECT STATUS COLORS
|
|
@@ -385,7 +385,7 @@
|
|
|
385
385
|
--support-activity-line-color: var(--support-border-color);
|
|
386
386
|
--support-activity-dot-default: var(--support-text-muted);
|
|
387
387
|
--support-activity-dot-status: var(--support-primary);
|
|
388
|
-
--support-activity-dot-assign: #
|
|
388
|
+
--support-activity-dot-assign: #17807c;
|
|
389
389
|
--support-activity-dot-merge: #0891b2;
|
|
390
390
|
|
|
391
391
|
/* ------------------------------------------
|
|
@@ -465,10 +465,10 @@
|
|
|
465
465
|
* Indicator for messages scheduled to be
|
|
466
466
|
* sent at a future date/time.
|
|
467
467
|
* ------------------------------------------ */
|
|
468
|
-
--support-scheduled-bg: #
|
|
468
|
+
--support-scheduled-bg: #eef8f7;
|
|
469
469
|
--support-scheduled-border: #a78bfa;
|
|
470
470
|
--support-scheduled-text: #5b21b6;
|
|
471
|
-
--support-scheduled-icon: #
|
|
471
|
+
--support-scheduled-icon: #17807c;
|
|
472
472
|
|
|
473
473
|
/* ------------------------------------------
|
|
474
474
|
* 34. EMAIL TRACKING
|
|
@@ -615,8 +615,8 @@
|
|
|
615
615
|
|
|
616
616
|
/* Scheduled */
|
|
617
617
|
--support-scheduled-bg: #2e1065;
|
|
618
|
-
--support-scheduled-border: #
|
|
619
|
-
--support-scheduled-text: #
|
|
618
|
+
--support-scheduled-border: #17807c;
|
|
619
|
+
--support-scheduled-text: #80bbb7;
|
|
620
620
|
|
|
621
621
|
/* Email tracking */
|
|
622
622
|
--support-email-pending: #64748b;
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { RateLimitStore } from './utils/rateLimiter'
|
|
2
|
+
import type { InboundEmailInput } from './utils/webhookSecurity'
|
|
3
|
+
import type { Payload, PayloadRequest } from 'payload'
|
|
4
|
+
|
|
1
5
|
// ─── Feature flags ───────────────────────────────────────
|
|
2
6
|
|
|
3
7
|
export interface SupportFeatures {
|
|
@@ -76,6 +80,62 @@ export interface EmailConfig {
|
|
|
76
80
|
replyTo?: string
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
export interface SmsSendResult {
|
|
84
|
+
sent: boolean
|
|
85
|
+
skipped?: boolean
|
|
86
|
+
error?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SmsAdapter {
|
|
90
|
+
isConfigured?: () => boolean
|
|
91
|
+
send: (input: {
|
|
92
|
+
message: string
|
|
93
|
+
to: string
|
|
94
|
+
payload: Payload
|
|
95
|
+
ticket: Record<string, unknown>
|
|
96
|
+
client: Record<string, unknown>
|
|
97
|
+
}) => Promise<SmsSendResult>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SupportCapabilities {
|
|
101
|
+
/** Optional SMS transport. The module never depends on a concrete provider. */
|
|
102
|
+
sms?: {
|
|
103
|
+
adapter: SmsAdapter
|
|
104
|
+
buildMessage?: (input: {
|
|
105
|
+
client: Record<string, unknown>
|
|
106
|
+
ticket: Record<string, unknown>
|
|
107
|
+
}) => string | Promise<string>
|
|
108
|
+
}
|
|
109
|
+
/** Native daily/weekly notification queue. */
|
|
110
|
+
digests?: boolean
|
|
111
|
+
/** Incoming email business adapter, protected and validated by the module. */
|
|
112
|
+
inboundEmail?: {
|
|
113
|
+
handle: (req: PayloadRequest, input: InboundEmailInput) => Promise<Response>
|
|
114
|
+
secret?: string
|
|
115
|
+
secretHeader?: string
|
|
116
|
+
}
|
|
117
|
+
/** Generate a short, editable display title after ticket creation. */
|
|
118
|
+
aiTitles?: {
|
|
119
|
+
generate: (payload: Payload, ticketId: number | string) => Promise<unknown>
|
|
120
|
+
}
|
|
121
|
+
/** Override the module's default resolved-ticket synthesis generator. */
|
|
122
|
+
aiSummaries?: {
|
|
123
|
+
generate: (payload: Payload, ticketId: number | string) => Promise<unknown>
|
|
124
|
+
}
|
|
125
|
+
/** Persist billing lines and their invoicing state. */
|
|
126
|
+
detailedBilling?: boolean
|
|
127
|
+
/** Persist pro-bono work and its estimated value. */
|
|
128
|
+
volunteering?: boolean
|
|
129
|
+
/** Best-effort cleanup of noisy messages after an inbound client reply. */
|
|
130
|
+
threadCleanup?: {
|
|
131
|
+
clean: (payload: Payload, ticketId: number | string, messageId: number | string) => Promise<unknown>
|
|
132
|
+
}
|
|
133
|
+
/** Deployment-specific project suggestion adapter. */
|
|
134
|
+
projectSuggestions?: {
|
|
135
|
+
suggest: (req: PayloadRequest) => Promise<Response>
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
79
139
|
// ─── Plugin configuration ────────────────────────────────
|
|
80
140
|
|
|
81
141
|
export interface SupportPluginConfig {
|
|
@@ -88,6 +148,15 @@ export interface SupportPluginConfig {
|
|
|
88
148
|
/** Email configuration for ticket notifications */
|
|
89
149
|
email?: EmailConfig
|
|
90
150
|
|
|
151
|
+
/** Shared rate-limit storage. Defaults to process-local memory. */
|
|
152
|
+
rateLimitStore?: RateLimitStore | 'payload'
|
|
153
|
+
|
|
154
|
+
/** Sequential ticket number formatting. */
|
|
155
|
+
ticketNumber?: { prefix?: string; padding?: number }
|
|
156
|
+
|
|
157
|
+
/** Optional generic capabilities backed by deployment-specific adapters. */
|
|
158
|
+
capabilities?: SupportCapabilities
|
|
159
|
+
|
|
91
160
|
/** Locale: 'fr' or 'en' (default: 'fr') */
|
|
92
161
|
locale?: 'fr' | 'en'
|
|
93
162
|
|
|
@@ -136,6 +205,8 @@ export interface SupportPluginConfig {
|
|
|
136
205
|
macros?: string
|
|
137
206
|
ticketStatuses?: string
|
|
138
207
|
ticketFeedback?: string
|
|
208
|
+
rateLimits?: string
|
|
209
|
+
counters?: string
|
|
139
210
|
}
|
|
140
211
|
|
|
141
212
|
/** Admin notification collection slug (default: 'admin-notifications') */
|
package/src/utils/rateLimiter.ts
CHANGED
|
@@ -1,52 +1,145 @@
|
|
|
1
|
+
export interface RateLimitEntry {
|
|
2
|
+
count: number
|
|
3
|
+
resetAt: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RateLimitStore {
|
|
7
|
+
increment(key: string, windowMs: number, context?: unknown): Promise<RateLimitEntry>
|
|
8
|
+
reset(key: string, context?: unknown): Promise<void>
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
12
|
+
* Process-local fallback store. Applications running more than one process
|
|
13
|
+
* should provide a persistent RateLimitStore through the plugin options.
|
|
4
14
|
*/
|
|
5
|
-
export class
|
|
6
|
-
private
|
|
15
|
+
export class MemoryRateLimitStore implements RateLimitStore {
|
|
16
|
+
private readonly entries = new Map<string, RateLimitEntry>()
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
|
|
19
|
+
const now = Date.now()
|
|
20
|
+
const current = this.entries.get(key)
|
|
21
|
+
const next = !current || now > current.resetAt
|
|
22
|
+
? { count: 1, resetAt: now + windowMs }
|
|
23
|
+
: { ...current, count: current.count + 1 }
|
|
24
|
+
|
|
25
|
+
this.entries.set(key, next)
|
|
26
|
+
return next
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
check(key: string): boolean {
|
|
22
|
-
const now = Date.now()
|
|
23
|
-
const entry = this.store.get(key)
|
|
29
|
+
async reset(key: string): Promise<void> {
|
|
30
|
+
this.entries.delete(key)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
interface PayloadRateLimitContext {
|
|
35
|
+
find(args: Record<string, unknown>): Promise<{ docs: Array<Record<string, unknown>> }>
|
|
36
|
+
create(args: Record<string, unknown>): Promise<Record<string, unknown>>
|
|
37
|
+
update(args: Record<string, unknown>): Promise<Record<string, unknown>>
|
|
38
|
+
delete(args: Record<string, unknown>): Promise<unknown>
|
|
39
|
+
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
type PayloadRateLimitRequest = Pick<PayloadRequest, 'payload' | 'transactionID'>
|
|
42
|
+
|
|
43
|
+
export class PayloadRateLimitStore implements RateLimitStore {
|
|
44
|
+
constructor(private readonly collectionSlug = 'support-rate-limits') {}
|
|
45
|
+
|
|
46
|
+
async increment(key: string, windowMs: number, context?: unknown): Promise<RateLimitEntry> {
|
|
47
|
+
const { payload, req } = this.resolveContext(context)
|
|
48
|
+
const ownsTransaction = req ? await initTransaction(req) : false
|
|
49
|
+
try {
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
const result = await payload.find({
|
|
52
|
+
collection: this.collectionSlug,
|
|
53
|
+
where: { key: { equals: key } },
|
|
54
|
+
limit: 1,
|
|
55
|
+
depth: 0,
|
|
56
|
+
overrideAccess: true,
|
|
57
|
+
...(req ? { req } : {}),
|
|
58
|
+
})
|
|
59
|
+
const current = result.docs[0]
|
|
60
|
+
const currentResetAt = current?.resetAt ? new Date(String(current.resetAt)).getTime() : 0
|
|
61
|
+
const next = !current || now > currentResetAt
|
|
62
|
+
? { count: 1, resetAt: now + windowMs }
|
|
63
|
+
: { count: Number(current.count || 0) + 1, resetAt: currentResetAt }
|
|
64
|
+
|
|
65
|
+
if (current?.id != null) {
|
|
66
|
+
await payload.update({
|
|
67
|
+
collection: this.collectionSlug,
|
|
68
|
+
id: current.id,
|
|
69
|
+
data: { count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
70
|
+
overrideAccess: true,
|
|
71
|
+
...(req ? { req } : {}),
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
await payload.create({
|
|
75
|
+
collection: this.collectionSlug,
|
|
76
|
+
data: { key, count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
77
|
+
overrideAccess: true,
|
|
78
|
+
...(req ? { req } : {}),
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (ownsTransaction && req) await commitTransaction(req)
|
|
83
|
+
return next
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (ownsTransaction && req) await killTransaction(req)
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
32
88
|
}
|
|
33
89
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
90
|
+
async reset(key: string, context?: unknown): Promise<void> {
|
|
91
|
+
const { payload, req } = this.resolveContext(context)
|
|
92
|
+
const ownsTransaction = req ? await initTransaction(req) : false
|
|
93
|
+
try {
|
|
94
|
+
await payload.delete({
|
|
95
|
+
collection: this.collectionSlug,
|
|
96
|
+
where: { key: { equals: key } },
|
|
97
|
+
overrideAccess: true,
|
|
98
|
+
...(req ? { req } : {}),
|
|
99
|
+
})
|
|
100
|
+
if (ownsTransaction && req) await commitTransaction(req)
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (ownsTransaction && req) await killTransaction(req)
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
39
105
|
}
|
|
40
106
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (now > entry.resetAt) {
|
|
48
|
-
this.store.delete(key)
|
|
49
|
-
}
|
|
107
|
+
private resolveContext(context: unknown): {
|
|
108
|
+
payload: PayloadRateLimitContext
|
|
109
|
+
req?: PayloadRateLimitRequest
|
|
110
|
+
} {
|
|
111
|
+
if (!context || typeof context !== 'object') {
|
|
112
|
+
throw new Error('PayloadRateLimitStore requires the current Payload request or instance')
|
|
50
113
|
}
|
|
114
|
+
if ('payload' in context) {
|
|
115
|
+
const req = context as PayloadRateLimitRequest
|
|
116
|
+
return { payload: req.payload as unknown as PayloadRateLimitContext, req }
|
|
117
|
+
}
|
|
118
|
+
return { payload: context as PayloadRateLimitContext }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class RateLimiter {
|
|
123
|
+
private readonly store: RateLimitStore
|
|
124
|
+
|
|
125
|
+
constructor(
|
|
126
|
+
private readonly windowMs: number,
|
|
127
|
+
private readonly maxRequests: number,
|
|
128
|
+
store?: RateLimitStore,
|
|
129
|
+
) {
|
|
130
|
+
this.store = store ?? new MemoryRateLimitStore()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async check(key: string, context?: unknown): Promise<boolean> {
|
|
134
|
+
const entry = context === undefined
|
|
135
|
+
? await this.store.increment(key, this.windowMs)
|
|
136
|
+
: await this.store.increment(key, this.windowMs, context)
|
|
137
|
+
return entry.count > this.maxRequests
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async reset(key: string, context?: unknown): Promise<void> {
|
|
141
|
+
if (context === undefined) await this.store.reset(key)
|
|
142
|
+
else await this.store.reset(key, context)
|
|
51
143
|
}
|
|
52
144
|
}
|
|
145
|
+
import { commitTransaction, initTransaction, killTransaction, type PayloadRequest } from 'payload'
|
package/src/utils/slugs.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface CollectionSlugs {
|
|
|
24
24
|
automationRules: string
|
|
25
25
|
supportTeams: string
|
|
26
26
|
pushSubscriptions: string
|
|
27
|
+
rateLimits: string
|
|
28
|
+
counters: string
|
|
27
29
|
users: string
|
|
28
30
|
media: string
|
|
29
31
|
}
|
|
@@ -50,6 +52,8 @@ export const DEFAULT_SLUGS: CollectionSlugs = {
|
|
|
50
52
|
automationRules: 'automation-rules',
|
|
51
53
|
supportTeams: 'support-teams',
|
|
52
54
|
pushSubscriptions: 'push-subscriptions',
|
|
55
|
+
rateLimits: 'support-rate-limits',
|
|
56
|
+
counters: 'support-counters',
|
|
53
57
|
users: 'users',
|
|
54
58
|
media: 'media',
|
|
55
59
|
}
|