@consilioweb/payload-support 2.0.1 → 4.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 +670 -402
- package/dist/components/RichTextEditor/index.d.ts +19 -0
- package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
- package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
- package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
- package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
- package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
- package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
- package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
- package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
- package/dist/components/TicketConversation/config.d.ts +10 -0
- package/dist/components/TicketConversation/config.js +1 -40
- package/dist/components/TicketConversation/constants.d.ts +56 -0
- package/dist/components/TicketConversation/context.d.ts +53 -0
- package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
- package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
- package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
- package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
- package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
- package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
- package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
- package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
- package/dist/components/TicketConversation/index.d.ts +4 -0
- package/dist/components/TicketConversation/index.js +2 -2
- package/dist/components/TicketConversation/types.d.ts +56 -0
- package/dist/components/TicketConversation/utils.d.ts +2 -0
- package/dist/index.cjs +940 -404
- package/dist/index.d.cts +119 -7
- package/dist/index.d.ts +119 -7
- package/dist/index.js +936 -405
- package/dist/utils/features.d.ts +123 -0
- package/dist/utils/features.js +147 -0
- package/dist/views/BillingView/client.d.ts +2 -0
- package/dist/views/BillingView/index.d.ts +4 -0
- package/dist/views/ChatView/client.d.ts +2 -0
- package/dist/views/ChatView/client.js +9 -9
- package/dist/views/ChatView/index.d.ts +4 -0
- package/dist/views/CrmView/client.d.ts +2 -0
- package/dist/views/CrmView/client.js +6 -9
- package/dist/views/CrmView/index.d.ts +4 -0
- package/dist/views/EmailTrackingView/client.d.ts +2 -0
- package/dist/views/EmailTrackingView/index.d.ts +4 -0
- package/dist/views/ImportConversationView/client.d.ts +2 -0
- package/dist/views/ImportConversationView/index.d.ts +4 -0
- package/dist/views/LogsView/client.d.ts +2 -0
- package/dist/views/LogsView/index.d.ts +4 -0
- package/dist/views/NewTicketView/client.d.ts +2 -0
- package/dist/views/NewTicketView/index.d.ts +4 -0
- package/dist/views/PendingEmailsView/client.d.ts +2 -0
- package/dist/views/PendingEmailsView/index.d.ts +4 -0
- package/dist/views/SupportDashboardView/client.d.ts +2 -0
- package/dist/views/SupportDashboardView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
- package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
- package/dist/views/TicketDetailView/client.d.ts +2 -0
- package/dist/views/TicketDetailView/client.js +2 -2
- package/dist/views/TicketDetailView/constants.d.ts +13 -0
- package/dist/views/TicketDetailView/helpers.d.ts +2 -0
- package/dist/views/TicketDetailView/index.d.ts +4 -0
- package/dist/views/TicketDetailView/types.d.ts +40 -0
- package/dist/views/TicketInboxView/client.d.ts +2 -0
- package/dist/views/TicketInboxView/client.js +5 -5
- package/dist/views/TicketInboxView/index.d.ts +4 -0
- package/dist/views/TicketingSettingsView/client.d.ts +2 -0
- package/dist/views/TicketingSettingsView/client.js +5 -4
- package/dist/views/TicketingSettingsView/index.d.ts +4 -0
- package/dist/views/TimeDashboardView/client.d.ts +2 -0
- package/dist/views/TimeDashboardView/index.d.ts +4 -0
- package/dist/views/shared/AdminViewHeader.d.ts +13 -0
- package/dist/views/shared/ErrorBoundary.d.ts +17 -0
- package/dist/views/shared/Skeleton.d.ts +21 -0
- package/dist/views/shared/StatusPill.d.ts +8 -0
- package/dist/views/shared/adminTokens.d.ts +19 -0
- package/dist/views/shared/config.d.ts +8 -0
- package/dist/views/shared/config.js +1 -40
- package/dist/views/shared/dateLocale.d.ts +5 -0
- package/dist/views/shared/index.d.ts +12 -0
- package/dist/views/shared/index.js +2 -1
- package/dist/views/shared/sla.d.ts +14 -0
- package/dist/views.d.ts +13 -13
- package/package.json +16 -31
- package/src/collections/ChatMessages.ts +59 -2
- package/src/collections/ClientSummaries.ts +10 -4
- package/src/collections/SupportClients.ts +29 -0
- package/src/collections/TicketCollaborators.ts +6 -1
- package/src/collections/TicketMessages.ts +49 -24
- package/src/collections/Tickets.ts +117 -79
- package/src/collections/TimeEntries.ts +57 -32
- package/src/collections/WebhookEndpoints.ts +44 -2
- package/src/components/TicketConversation/config.ts +16 -80
- package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
- package/src/components/TicketConversation/index.tsx +2 -2
- package/src/endpoints/admin-chat.ts +3 -3
- package/src/endpoints/ai-agent.ts +3 -3
- package/src/endpoints/ai.ts +5 -4
- package/src/endpoints/auth-2fa.ts +16 -4
- package/src/endpoints/capabilities.ts +6 -6
- package/src/endpoints/chat.ts +7 -5
- package/src/endpoints/chatbot.ts +48 -2
- package/src/endpoints/client-intelligence.ts +6 -5
- package/src/endpoints/email-stats.ts +19 -3
- package/src/endpoints/import-conversation.ts +1 -1
- package/src/endpoints/index.ts +1 -1
- package/src/endpoints/invite-collaborator.ts +29 -3
- package/src/endpoints/login.ts +15 -2
- package/src/endpoints/oauth-google.ts +130 -8
- package/src/endpoints/process-scheduled.ts +13 -8
- package/src/endpoints/resend-notification.ts +3 -3
- package/src/endpoints/round-robin-config.ts +30 -20
- package/src/endpoints/send-reminder.ts +3 -3
- package/src/endpoints/settings.ts +28 -77
- package/src/endpoints/signature.ts +9 -2
- package/src/endpoints/ticket-synthesis.ts +3 -3
- package/src/endpoints/transfer-ticket.ts +28 -3
- package/src/endpoints/typing.ts +117 -14
- package/src/endpoints/user-prefs.ts +5 -2
- package/src/index.ts +6 -2
- package/src/plugin.ts +12 -0
- package/src/portal/LiveChat.tsx +33 -22
- package/src/portal/auth/layout.tsx +19 -1
- package/src/portal/auth/tickets/detail/MessageBody.tsx +88 -0
- package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
- package/src/portal/auth/tickets/detail/page.tsx +2 -6
- package/src/portal/login/page.tsx +11 -3
- package/src/types/lucide-react.d.ts +10 -2
- package/src/utils/aiAgent.ts +2 -1
- package/src/utils/aiProvider.ts +21 -0
- package/src/utils/features.ts +297 -0
- package/src/utils/fireWebhooks.ts +10 -1
- package/src/utils/generateTicketSynthesis.ts +2 -1
- package/src/utils/index.ts +4 -2
- package/src/utils/rateLimiter.ts +39 -5
- package/src/utils/readSettings.ts +199 -16
- package/src/utils/ticketAccess.ts +16 -1
- package/src/utils/twoFactorChallenge.ts +80 -0
- package/src/utils/urlSafety.ts +230 -0
- package/src/utils/webhookDispatcher.ts +24 -8
- package/src/views/ChatView/client.tsx +13 -9
- package/src/views/CrmView/client.tsx +10 -11
- package/src/views/TicketDetailView/client.tsx +2 -2
- package/src/views/TicketInboxView/client.tsx +10 -5
- package/src/views/TicketingSettingsView/client.tsx +8 -6
- package/src/views/shared/config.ts +14 -80
- package/src/views/shared/index.ts +3 -1
- package/dist/components/RichTextEditor/index.cjs +0 -279
- package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
- package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
- package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
- package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
- package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
- package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
- package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
- package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
- package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
- package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
- package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
- package/dist/components/TicketConversation/config.cjs +0 -44
- package/dist/components/TicketConversation/constants.cjs +0 -127
- package/dist/components/TicketConversation/context.cjs +0 -13
- package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
- package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
- package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
- package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
- package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
- package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
- package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -101
- package/dist/components/TicketConversation/index.cjs +0 -1114
- package/dist/components/TicketConversation/types.cjs +0 -2
- package/dist/components/TicketConversation/utils.cjs +0 -27
- package/dist/views/BillingView/client.cjs +0 -469
- package/dist/views/BillingView/index.cjs +0 -34
- package/dist/views/ChatView/client.cjs +0 -301
- package/dist/views/ChatView/index.cjs +0 -34
- package/dist/views/CrmView/client.cjs +0 -458
- package/dist/views/CrmView/index.cjs +0 -34
- package/dist/views/EmailTrackingView/client.cjs +0 -171
- package/dist/views/EmailTrackingView/index.cjs +0 -34
- package/dist/views/ImportConversationView/client.cjs +0 -238
- package/dist/views/ImportConversationView/index.cjs +0 -34
- package/dist/views/LogsView/client.cjs +0 -148
- package/dist/views/LogsView/index.cjs +0 -32
- package/dist/views/NewTicketView/client.cjs +0 -213
- package/dist/views/NewTicketView/index.cjs +0 -32
- package/dist/views/PendingEmailsView/client.cjs +0 -471
- package/dist/views/PendingEmailsView/index.cjs +0 -34
- package/dist/views/SupportDashboardView/client.cjs +0 -437
- package/dist/views/SupportDashboardView/index.cjs +0 -34
- package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
- package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
- package/dist/views/TicketDetailView/client.cjs +0 -1412
- package/dist/views/TicketDetailView/constants.cjs +0 -20
- package/dist/views/TicketDetailView/helpers.cjs +0 -23
- package/dist/views/TicketDetailView/index.cjs +0 -34
- package/dist/views/TicketDetailView/types.cjs +0 -2
- package/dist/views/TicketInboxView/client.cjs +0 -359
- package/dist/views/TicketInboxView/index.cjs +0 -32
- package/dist/views/TicketingSettingsView/client.cjs +0 -740
- package/dist/views/TicketingSettingsView/index.cjs +0 -34
- package/dist/views/TimeDashboardView/client.cjs +0 -180
- package/dist/views/TimeDashboardView/index.cjs +0 -34
- package/dist/views/shared/AdminViewHeader.cjs +0 -68
- package/dist/views/shared/ErrorBoundary.cjs +0 -55
- package/dist/views/shared/Skeleton.cjs +0 -77
- package/dist/views/shared/StatusPill.cjs +0 -47
- package/dist/views/shared/adminTokens.cjs +0 -34
- package/dist/views/shared/config.cjs +0 -44
- package/dist/views/shared/dateLocale.cjs +0 -5
- package/dist/views/shared/index.cjs +0 -72
- package/dist/views/shared/sla.cjs +0 -37
- package/dist/views.cjs +0 -86
- package/dist/views.d.cts +0 -13
- package/src/__tests__/aiSummaryRendering.test.ts +0 -21
- package/src/__tests__/authResponses.test.ts +0 -69
- package/src/__tests__/capabilities.test.ts +0 -71
- package/src/__tests__/generateTrackingToken.test.ts +0 -67
- package/src/__tests__/integration/ai-agent.test.ts +0 -46
- package/src/__tests__/integration/auth.test.ts +0 -43
- package/src/__tests__/integration/automation-rules.test.ts +0 -68
- package/src/__tests__/integration/buildTestPayload.ts +0 -52
- package/src/__tests__/integration/channels.test.ts +0 -52
- package/src/__tests__/integration/core-behaviors.test.ts +0 -81
- package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
- package/src/__tests__/integration/digest.test.ts +0 -48
- package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
- package/src/__tests__/integration/isolation.test.ts +0 -81
- package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
- package/src/__tests__/integration/nps.test.ts +0 -37
- package/src/__tests__/integration/per-team-sla.test.ts +0 -44
- package/src/__tests__/integration/push.test.ts +0 -75
- package/src/__tests__/integration/sanitization.test.ts +0 -35
- package/src/__tests__/integration/sla-pause.test.ts +0 -39
- package/src/__tests__/integration/smoke.test.ts +0 -26
- package/src/__tests__/integration/snooze.test.ts +0 -68
- package/src/__tests__/integration/teams.test.ts +0 -38
- package/src/__tests__/rateLimiter.test.ts +0 -83
- package/src/__tests__/sanitizeHtml.test.ts +0 -165
- package/src/__tests__/sla.test.ts +0 -258
- package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
- package/src/__tests__/translations.test.ts +0 -67
- package/src/__tests__/webhookSecurity.test.ts +0 -58
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { Where } from 'payload'
|
|
3
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
4
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
4
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
5
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
6
6
|
import { dbFind, dbCreate, dbUpdate } from '../utils/db'
|
|
7
7
|
|
|
@@ -111,7 +111,7 @@ export function createAdminChatGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
111
111
|
* Admin sends a message or closes a session.
|
|
112
112
|
*/
|
|
113
113
|
export function createAdminChatPostEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
114
|
-
const adminChatLimiter = new RateLimiter(60_000, 30, store)
|
|
114
|
+
const adminChatLimiter = new RateLimiter(60_000, 30, store, 'admin-chat')
|
|
115
115
|
return {
|
|
116
116
|
path: '/support/admin-chat',
|
|
117
117
|
method: 'post',
|
|
@@ -150,7 +150,7 @@ export function createAdminChatPostEndpoint(slugs: CollectionSlugs, store?: Rate
|
|
|
150
150
|
|
|
151
151
|
// Agent sends a message
|
|
152
152
|
if (action === 'send' && message) {
|
|
153
|
-
if (await adminChatLimiter.check(
|
|
153
|
+
if (await adminChatLimiter.check(principalRateKey(req.user), req)) {
|
|
154
154
|
return Response.json({ error: 'Rate limit atteint.' }, { status: 429 })
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -2,7 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { runAiAgent } from '../utils/aiAgent'
|
|
5
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* POST /api/support/ai-agent body: { ticketId, confidenceThreshold? }
|
|
@@ -11,14 +11,14 @@ import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
|
11
11
|
* (when confident) or escalates to a human with an internal note. Admin-only.
|
|
12
12
|
*/
|
|
13
13
|
export function createAiAgentEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
14
|
-
const limiter = new RateLimiter(60_000, 10, store)
|
|
14
|
+
const limiter = new RateLimiter(60_000, 10, store, 'ai-agent')
|
|
15
15
|
return {
|
|
16
16
|
path: '/support/ai-agent',
|
|
17
17
|
method: 'post',
|
|
18
18
|
handler: async (req) => {
|
|
19
19
|
try {
|
|
20
20
|
requireAdmin(req, slugs)
|
|
21
|
-
if (await limiter.check(
|
|
21
|
+
if (await limiter.check(principalRateKey(req.user), req)) {
|
|
22
22
|
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
|
|
23
23
|
}
|
|
24
24
|
let body: { ticketId?: number | string; confidenceThreshold?: number } = {}
|
package/src/endpoints/ai.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
|
|
5
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
|
+
import { resolveOllamaBaseUrl } from '../utils/aiProvider'
|
|
6
7
|
|
|
7
8
|
async function getClient(aiSettings: SupportSettings['ai']) {
|
|
8
9
|
const moduleName = '@anthropic-ai/sdk'
|
|
9
10
|
const { default: Anthropic } = await import(moduleName)
|
|
10
11
|
if (aiSettings.provider === 'ollama') {
|
|
11
|
-
const baseURL =
|
|
12
|
+
const baseURL = resolveOllamaBaseUrl()
|
|
12
13
|
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
13
14
|
}
|
|
14
15
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
@@ -25,7 +26,7 @@ type AiAction = 'sentiment' | 'synthesis' | 'suggest_reply' | 'rewrite'
|
|
|
25
26
|
* Admin-only endpoint for AI features in support.
|
|
26
27
|
*/
|
|
27
28
|
export function createAiEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
28
|
-
const limiter = new RateLimiter(60_000, 30, store)
|
|
29
|
+
const limiter = new RateLimiter(60_000, 30, store, 'ai')
|
|
29
30
|
return {
|
|
30
31
|
path: '/support/ai',
|
|
31
32
|
method: 'post',
|
|
@@ -34,7 +35,7 @@ export function createAiEndpoint(slugs: CollectionSlugs, store?: RateLimitStore)
|
|
|
34
35
|
const payload = req.payload
|
|
35
36
|
|
|
36
37
|
requireAdmin(req, slugs)
|
|
37
|
-
if (await limiter.check(
|
|
38
|
+
if (await limiter.check(principalRateKey(req.user), req)) {
|
|
38
39
|
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -4,6 +4,7 @@ import crypto, { createHmac } from 'crypto'
|
|
|
4
4
|
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
5
|
import { escapeHtml } from '../utils/emailTemplate'
|
|
6
6
|
import { dbFind, dbUpdate } from '../utils/db'
|
|
7
|
+
import { verifyTwoFactorChallenge } from '../utils/twoFactorChallenge'
|
|
7
8
|
|
|
8
9
|
function generateSecureCode(): string {
|
|
9
10
|
const buf = crypto.randomBytes(4)
|
|
@@ -25,21 +26,21 @@ function hashCode(code: string): string {
|
|
|
25
26
|
* Send or verify a 2FA code.
|
|
26
27
|
*/
|
|
27
28
|
export function createAuth2faEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
28
|
-
const sendLimiter = new RateLimiter(60 * 60 * 1000, 3, store)
|
|
29
|
-
const verifyLimiter = new RateLimiter(15 * 60 * 1000, 5, store)
|
|
29
|
+
const sendLimiter = new RateLimiter(60 * 60 * 1000, 3, store, '2fa:send')
|
|
30
|
+
const verifyLimiter = new RateLimiter(15 * 60 * 1000, 5, store, '2fa:verify')
|
|
30
31
|
return {
|
|
31
32
|
path: '/support/2fa',
|
|
32
33
|
method: 'post',
|
|
33
34
|
handler: async (req) => {
|
|
34
35
|
try {
|
|
35
36
|
const payload = req.payload
|
|
36
|
-
let body: { action?: string; email?: string; code?: string }
|
|
37
|
+
let body: { action?: string; email?: string; code?: string; challenge?: string }
|
|
37
38
|
try {
|
|
38
39
|
body = await req.json!()
|
|
39
40
|
} catch {
|
|
40
41
|
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
41
42
|
}
|
|
42
|
-
const { action, email, code } = body
|
|
43
|
+
const { action, email, code, challenge } = body
|
|
43
44
|
|
|
44
45
|
if (!action || !email) {
|
|
45
46
|
return Response.json({ error: 'Paramètres manquants' }, { status: 400 })
|
|
@@ -48,6 +49,17 @@ export function createAuth2faEndpoint(slugs: CollectionSlugs, store?: RateLimitS
|
|
|
48
49
|
const genericSendResponse = { success: true, message: 'Si un compte existe, un code a été envoyé.' }
|
|
49
50
|
|
|
50
51
|
if (action === 'send') {
|
|
52
|
+
// The challenge is checked BEFORE the limiter on purpose: the limiter
|
|
53
|
+
// is keyed on the VICTIM's email, so letting an unauthenticated caller
|
|
54
|
+
// reach it is what made the lockout possible. No challenge, no budget
|
|
55
|
+
// consumed, no mail sent, no code overwritten.
|
|
56
|
+
if (!verifyTwoFactorChallenge(email, challenge)) {
|
|
57
|
+
return Response.json(
|
|
58
|
+
{ error: 'Authentification requise avant l\'envoi d\'un code.' },
|
|
59
|
+
{ status: 401 },
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
if (await sendLimiter.check(email, req)) {
|
|
52
64
|
return Response.json(genericSendResponse)
|
|
53
65
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Endpoint, Where } from 'payload'
|
|
2
2
|
import type { SupportCapabilities } from '../types'
|
|
3
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
4
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
4
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
5
|
import {
|
|
6
6
|
validateInboundEmailPayload,
|
|
7
7
|
verifySecret,
|
|
@@ -12,7 +12,7 @@ export function createInboundEmailEndpoint(
|
|
|
12
12
|
capability: NonNullable<SupportCapabilities['inboundEmail']>,
|
|
13
13
|
store?: RateLimitStore,
|
|
14
14
|
): Endpoint {
|
|
15
|
-
const limiter = new RateLimiter(60_000, 60, store)
|
|
15
|
+
const limiter = new RateLimiter(60_000, 60, store, 'inbound-email')
|
|
16
16
|
return {
|
|
17
17
|
path: '/support-webhook/inbound-email',
|
|
18
18
|
method: 'post',
|
|
@@ -51,7 +51,7 @@ export function createProjectSuggestionsEndpoint(
|
|
|
51
51
|
capability: NonNullable<SupportCapabilities['projectSuggestions']>,
|
|
52
52
|
store?: RateLimitStore,
|
|
53
53
|
): Endpoint {
|
|
54
|
-
const limiter = new RateLimiter(60_000, 20, store)
|
|
54
|
+
const limiter = new RateLimiter(60_000, 20, store, 'suggest-projects')
|
|
55
55
|
return {
|
|
56
56
|
path: '/support/suggest-projects',
|
|
57
57
|
method: 'post',
|
|
@@ -59,7 +59,7 @@ export function createProjectSuggestionsEndpoint(
|
|
|
59
59
|
if (!req.user || req.user.collection !== slugs.users) {
|
|
60
60
|
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
61
61
|
}
|
|
62
|
-
const key =
|
|
62
|
+
const key = principalRateKey(req.user)
|
|
63
63
|
if (await limiter.check(key, req)) {
|
|
64
64
|
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
|
|
65
65
|
}
|
|
@@ -73,7 +73,7 @@ export function createTicketTitleEndpoint(
|
|
|
73
73
|
capability: NonNullable<SupportCapabilities['aiTitles']>,
|
|
74
74
|
store?: RateLimitStore,
|
|
75
75
|
): Endpoint {
|
|
76
|
-
const limiter = new RateLimiter(60_000, 20, store)
|
|
76
|
+
const limiter = new RateLimiter(60_000, 20, store, 'ticket-title')
|
|
77
77
|
return {
|
|
78
78
|
path: '/support/ticket-title',
|
|
79
79
|
method: 'post',
|
|
@@ -100,7 +100,7 @@ export function createGenerateMissingTitlesEndpoint(
|
|
|
100
100
|
capability: NonNullable<SupportCapabilities['aiTitles']>,
|
|
101
101
|
store?: RateLimitStore,
|
|
102
102
|
): Endpoint {
|
|
103
|
-
const limiter = new RateLimiter(60_000, 5, store)
|
|
103
|
+
const limiter = new RateLimiter(60_000, 5, store, 'generate-missing-titles')
|
|
104
104
|
return {
|
|
105
105
|
path: '/support/generate-missing-titles',
|
|
106
106
|
method: 'post',
|
package/src/endpoints/chat.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { Where } from 'payload'
|
|
3
3
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
4
4
|
import crypto from 'crypto'
|
|
5
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
6
|
import { requireClient, handleAuthError } from '../utils/auth'
|
|
7
7
|
import { dbFind, dbCreate } from '../utils/db'
|
|
8
8
|
|
|
@@ -67,8 +67,8 @@ export function createChatGetEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
67
67
|
* Send a message or start a new chat session. Client-only.
|
|
68
68
|
*/
|
|
69
69
|
export function createChatPostEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
70
|
-
const chatSessionLimiter = new RateLimiter(3_600_000, 5, store)
|
|
71
|
-
const chatMessageLimiter = new RateLimiter(60_000, 15, store)
|
|
70
|
+
const chatSessionLimiter = new RateLimiter(3_600_000, 5, store, 'chat:session')
|
|
71
|
+
const chatMessageLimiter = new RateLimiter(60_000, 15, store, 'chat:message')
|
|
72
72
|
return {
|
|
73
73
|
path: '/support/chat',
|
|
74
74
|
method: 'post',
|
|
@@ -86,10 +86,12 @@ export function createChatPostEndpoint(slugs: CollectionSlugs, store?: RateLimit
|
|
|
86
86
|
}
|
|
87
87
|
const { action, session, message } = body
|
|
88
88
|
const userId = String(req.user.id)
|
|
89
|
+
// Collection-qualified: agent #7 and support-client #7 must not share a budget.
|
|
90
|
+
const rateKey = principalRateKey(req.user)
|
|
89
91
|
|
|
90
92
|
// Start a new session
|
|
91
93
|
if (action === 'start') {
|
|
92
|
-
if (await chatSessionLimiter.check(
|
|
94
|
+
if (await chatSessionLimiter.check(rateKey, req)) {
|
|
93
95
|
return Response.json({ error: 'Trop de sessions créées. Réessayez plus tard.' }, { status: 429 })
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -111,7 +113,7 @@ export function createChatPostEndpoint(slugs: CollectionSlugs, store?: RateLimit
|
|
|
111
113
|
|
|
112
114
|
// Send a message
|
|
113
115
|
if (action === 'send' && session && message) {
|
|
114
|
-
if (await chatMessageLimiter.check(
|
|
116
|
+
if (await chatMessageLimiter.check(rateKey, req)) {
|
|
115
117
|
return Response.json({ error: 'Trop de messages. Attendez un moment.' }, { status: 429 })
|
|
116
118
|
}
|
|
117
119
|
|
package/src/endpoints/chatbot.ts
CHANGED
|
@@ -4,13 +4,38 @@ import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
|
4
4
|
import { dbFind } from '../utils/db'
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Global hourly ceiling on chatbot calls, all callers combined.
|
|
9
|
+
*
|
|
10
|
+
* The per-IP window below is keyed on the FIRST hop of `X-Forwarded-For`, a
|
|
11
|
+
* value the caller sends itself: an anonymous client rotating that header gets a
|
|
12
|
+
* fresh window on every request. Unlike `login.ts` — where the account lock is
|
|
13
|
+
* the primary control — this endpoint has no second line of defence, and every
|
|
14
|
+
* accepted call ships ~50 KB of knowledge base to the Anthropic API on the
|
|
15
|
+
* operator's key. This unkeyed ceiling makes the bill bounded even when the
|
|
16
|
+
* per-IP key is bypassed. Raise it with `SUPPORT_CHATBOT_MAX_PER_HOUR` on a
|
|
17
|
+
* high-traffic portal, or pass `maxPerHour` when building the endpoint.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_CHATBOT_MAX_PER_HOUR = 200
|
|
20
|
+
|
|
21
|
+
function resolveMaxPerHour(explicit?: number): number {
|
|
22
|
+
if (typeof explicit === 'number' && explicit > 0) return explicit
|
|
23
|
+
const fromEnv = Number(process.env.SUPPORT_CHATBOT_MAX_PER_HOUR)
|
|
24
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_CHATBOT_MAX_PER_HOUR
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
/**
|
|
8
28
|
* POST /api/support/chatbot
|
|
9
29
|
* AI chatbot that answers from the knowledge base before creating a ticket.
|
|
10
30
|
* Public endpoint (accessible from the support portal).
|
|
11
31
|
*/
|
|
12
|
-
export function createChatbotEndpoint(
|
|
13
|
-
|
|
32
|
+
export function createChatbotEndpoint(
|
|
33
|
+
slugs: CollectionSlugs,
|
|
34
|
+
store?: RateLimitStore,
|
|
35
|
+
maxPerHour?: number,
|
|
36
|
+
): Endpoint {
|
|
37
|
+
const chatbotLimiter = new RateLimiter(60_000, 10, store, 'chatbot:ip')
|
|
38
|
+
const globalLimiter = new RateLimiter(60 * 60_000, resolveMaxPerHour(maxPerHour), store, 'chatbot:global')
|
|
14
39
|
return {
|
|
15
40
|
path: '/support/chatbot',
|
|
16
41
|
method: 'post',
|
|
@@ -33,6 +58,27 @@ export function createChatbotEndpoint(slugs: CollectionSlugs, store?: RateLimitS
|
|
|
33
58
|
return Response.json({ error: 'Question too short' }, { status: 400 })
|
|
34
59
|
}
|
|
35
60
|
|
|
61
|
+
// Not keyed on anything the caller controls — see the note above.
|
|
62
|
+
// Placed after validation so malformed requests do not eat the budget.
|
|
63
|
+
//
|
|
64
|
+
// Over budget, the answer DEGRADES, it does not fail: a shared counter
|
|
65
|
+
// that returns 429 hands an anonymous visitor a switch to turn the
|
|
66
|
+
// chatbot off for everybody (the per-IP key above is spoofable, so
|
|
67
|
+
// exhausting the ceiling costs nothing). The deflection path — "no
|
|
68
|
+
// answer here, open a ticket" — is the exact response this endpoint
|
|
69
|
+
// already returns when the knowledge base is empty or the API key is
|
|
70
|
+
// missing, so no caller learns a new shape, and the bill stays capped
|
|
71
|
+
// because the AI call below is never reached.
|
|
72
|
+
if (await globalLimiter.check('all', req)) {
|
|
73
|
+
return Response.json({
|
|
74
|
+
answer: null,
|
|
75
|
+
confidence: 0,
|
|
76
|
+
suggestion: 'create_ticket',
|
|
77
|
+
aiUnavailable: true,
|
|
78
|
+
message: 'L\'assistant est momentanément indisponible. Créez un ticket, un agent vous répondra.',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
const payload = req.payload
|
|
37
83
|
|
|
38
84
|
const articles = await dbFind(payload, slugs.knowledgeBase, {
|
|
@@ -2,13 +2,14 @@ import type { Endpoint, PayloadRequest } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { requireAdmin, handleAuthError } from '../utils/auth'
|
|
4
4
|
import { readSupportSettings, type SupportSettings } from '../utils/readSettings'
|
|
5
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
5
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
6
|
+
import { resolveOllamaBaseUrl } from '../utils/aiProvider'
|
|
6
7
|
|
|
7
8
|
async function getClient(aiSettings: SupportSettings['ai']) {
|
|
8
9
|
const moduleName = '@anthropic-ai/sdk'
|
|
9
10
|
const { default: Anthropic } = await import(moduleName)
|
|
10
11
|
if (aiSettings.provider === 'ollama') {
|
|
11
|
-
const baseURL =
|
|
12
|
+
const baseURL = resolveOllamaBaseUrl()
|
|
12
13
|
return new Anthropic({ apiKey: 'ollama', baseURL })
|
|
13
14
|
}
|
|
14
15
|
return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
@@ -28,11 +29,11 @@ const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
|
28
29
|
* Force-refreshes the summary.
|
|
29
30
|
*/
|
|
30
31
|
export function createClientIntelligenceEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint[] {
|
|
31
|
-
const limiter = new RateLimiter(60_000, 20, store)
|
|
32
|
+
const limiter = new RateLimiter(60_000, 20, store, 'client-intelligence')
|
|
32
33
|
const getHandler = async (req: PayloadRequest) => {
|
|
33
34
|
try {
|
|
34
35
|
requireAdmin(req, slugs)
|
|
35
|
-
if (await limiter.check(
|
|
36
|
+
if (await limiter.check(principalRateKey(req.user), req)) {
|
|
36
37
|
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
|
|
37
38
|
}
|
|
38
39
|
const payload = req.payload
|
|
@@ -70,7 +71,7 @@ export function createClientIntelligenceEndpoint(slugs: CollectionSlugs, store?:
|
|
|
70
71
|
const postHandler = async (req: PayloadRequest) => {
|
|
71
72
|
try {
|
|
72
73
|
requireAdmin(req, slugs)
|
|
73
|
-
if (await limiter.check(
|
|
74
|
+
if (await limiter.check(principalRateKey(req.user), req)) {
|
|
74
75
|
return Response.json({ error: 'Rate limit exceeded' }, { status: 429 })
|
|
75
76
|
}
|
|
76
77
|
const payload = req.payload
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload'
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
|
+
import { handleAuthError, requireAdmin } from '../utils/auth'
|
|
4
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
3
5
|
import { dbFind } from '../utils/db'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* GET /api/support/email-stats?days=7
|
|
7
9
|
* Aggregates EmailLogs data for the tracking dashboard. Admin-only.
|
|
10
|
+
*
|
|
11
|
+
* The guard used to be `!!req.user`, which ANY authenticated principal
|
|
12
|
+
* satisfies — a support-client, or a member of any other auth collection of the
|
|
13
|
+
* host app. That leaked the operational aggregate of `email-logs` (volume,
|
|
14
|
+
* failure rate, per-day and per-action breakdown) even though the collection's
|
|
15
|
+
* own `read` is staff-only, because the pages below are fetched with
|
|
16
|
+
* `overrideAccess: true`. It also handed any account an amplification primitive:
|
|
17
|
+
* up to 25 000 rows read and materialised per call, with no rate limit.
|
|
8
18
|
*/
|
|
9
|
-
export function createEmailStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
19
|
+
export function createEmailStatsEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
20
|
+
const statsLimiter = new RateLimiter(60_000, 20, store, 'email-stats')
|
|
10
21
|
return {
|
|
11
22
|
path: '/support/email-stats',
|
|
12
23
|
method: 'get',
|
|
@@ -14,8 +25,11 @@ export function createEmailStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
14
25
|
try {
|
|
15
26
|
const payload = req.payload
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
requireAdmin(req, slugs)
|
|
29
|
+
|
|
30
|
+
// Keyed on the authenticated user id, not on a spoofable header.
|
|
31
|
+
if (await statsLimiter.check(principalRateKey(req.user), req)) {
|
|
32
|
+
return Response.json({ error: 'Too many requests.' }, { status: 429 })
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
const url = new URL(req.url!)
|
|
@@ -101,6 +115,8 @@ export function createEmailStatsEndpoint(slugs: CollectionSlugs): Endpoint {
|
|
|
101
115
|
actions: Object.fromEntries(actionMap),
|
|
102
116
|
})
|
|
103
117
|
} catch (err) {
|
|
118
|
+
const authResponse = handleAuthError(err)
|
|
119
|
+
if (authResponse) return authResponse
|
|
104
120
|
console.error('[email-stats] Error:', err)
|
|
105
121
|
return Response.json({ error: 'Internal server error' }, { status: 500 })
|
|
106
122
|
}
|
|
@@ -132,7 +132,7 @@ ONLY JSON, nothing else.`,
|
|
|
132
132
|
* Import a conversation from markdown into the ticket system.
|
|
133
133
|
*/
|
|
134
134
|
export function createImportConversationEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
135
|
-
const importLimiter = new RateLimiter(3_600_000, 10, store)
|
|
135
|
+
const importLimiter = new RateLimiter(3_600_000, 10, store, 'import-conversation')
|
|
136
136
|
return {
|
|
137
137
|
path: '/support/import-conversation',
|
|
138
138
|
method: 'post',
|
package/src/endpoints/index.ts
CHANGED
|
@@ -199,7 +199,7 @@ export function createSupportEndpoints(slugs: CollectionSlugs, options?: Support
|
|
|
199
199
|
}
|
|
200
200
|
if (!f || f.satisfaction !== false) endpoints.push(createSatisfactionEndpoint(slugs))
|
|
201
201
|
if (!f || f.emailTracking !== false) {
|
|
202
|
-
endpoints.push(createEmailStatsEndpoint(slugs), createTrackOpenEndpoint(slugs))
|
|
202
|
+
endpoints.push(createEmailStatsEndpoint(slugs, rateLimitStore), createTrackOpenEndpoint(slugs))
|
|
203
203
|
}
|
|
204
204
|
if (!f || f.pendingEmails !== false) endpoints.push(createPendingEmailsProcessEndpoint(slugs))
|
|
205
205
|
if (!f || f.scheduledReplies !== false) endpoints.push(createProcessScheduledEndpoint(slugs))
|
|
@@ -4,7 +4,7 @@ import { handleAuthError, AuthError } from '../utils/auth'
|
|
|
4
4
|
import { escapeHtml, emailWrapper, emailButton, emailParagraph } from '../utils/emailTemplate'
|
|
5
5
|
import { readSupportSettings } from '../utils/readSettings'
|
|
6
6
|
import { randomBytes } from 'crypto'
|
|
7
|
-
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
7
|
+
import { principalRateKey, RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
8
8
|
import { dbFindByID, dbFind, dbCreate, dbUpdate, dbCount } from '../utils/db'
|
|
9
9
|
|
|
10
10
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
@@ -13,6 +13,21 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
|
13
13
|
// email, so cap both the volume per user and the collaborators per ticket.
|
|
14
14
|
const MAX_COLLABORATORS_PER_TICKET = 20
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Long-window ceiling on ACCOUNT CREATIONS per inviter.
|
|
18
|
+
*
|
|
19
|
+
* The two existing quotas are per hour (10 invites) and per ticket (20
|
|
20
|
+
* collaborators); neither bounds the total number of `support-clients` rows a
|
|
21
|
+
* single client can conjure, nor the total number of third parties it can mail,
|
|
22
|
+
* since a client can open as many tickets as it wants. This one is only consumed
|
|
23
|
+
* when the invited address has NO account yet — inviting an existing client
|
|
24
|
+
* (the normal case) never touches it.
|
|
25
|
+
*
|
|
26
|
+
* Applied to support-clients only: staff onboarding is a legitimate bulk flow.
|
|
27
|
+
*/
|
|
28
|
+
const MAX_NEW_ACCOUNTS_PER_INVITER = 30
|
|
29
|
+
const NEW_ACCOUNT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
|
|
30
|
+
|
|
16
31
|
/**
|
|
17
32
|
* POST /api/support/tickets/:id/invite
|
|
18
33
|
*
|
|
@@ -31,7 +46,8 @@ const MAX_COLLABORATORS_PER_TICKET = 20
|
|
|
31
46
|
* - Returns { ok, invitedTo, role }.
|
|
32
47
|
*/
|
|
33
48
|
export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
34
|
-
const inviteLimiter = new RateLimiter(60 * 60 * 1000, 10, store)
|
|
49
|
+
const inviteLimiter = new RateLimiter(60 * 60 * 1000, 10, store, 'invite-collaborator')
|
|
50
|
+
const newAccountLimiter = new RateLimiter(NEW_ACCOUNT_WINDOW_MS, MAX_NEW_ACCOUNTS_PER_INVITER, store, 'invite-collaborator:new-account')
|
|
35
51
|
return {
|
|
36
52
|
path: '/support/tickets/:id/invite',
|
|
37
53
|
method: 'post',
|
|
@@ -75,7 +91,7 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs, store?:
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
// Anti-abuse: cap invite volume per user (in-memory, fail-closed).
|
|
78
|
-
if (await inviteLimiter.check(
|
|
94
|
+
if (await inviteLimiter.check(principalRateKey(req.user), req)) {
|
|
79
95
|
return Response.json({ error: 'Trop d\'invitations. Réessayez plus tard.' }, { status: 429 })
|
|
80
96
|
}
|
|
81
97
|
|
|
@@ -107,6 +123,16 @@ export function createInviteCollaboratorEndpoint(slugs: CollectionSlugs, store?:
|
|
|
107
123
|
if (existing.docs.length > 0) {
|
|
108
124
|
inviteeId = (existing.docs[0] as any).id
|
|
109
125
|
} else {
|
|
126
|
+
// Creating an account for an address nobody vouched for — bounded over a
|
|
127
|
+
// long window so the endpoint cannot be used to mass-create ghost
|
|
128
|
+
// accounts and mail arbitrary third parties.
|
|
129
|
+
if (!isAdmin && await newAccountLimiter.check(principalRateKey(req.user), req)) {
|
|
130
|
+
return Response.json(
|
|
131
|
+
{ error: 'Trop de nouveaux comptes invités. Réessayez plus tard.' },
|
|
132
|
+
{ status: 429 },
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
110
136
|
// Create a placeholder client; password is random — the invitee will reset
|
|
111
137
|
// via the forgotPassword flow triggered by the SupportClients afterChange hook.
|
|
112
138
|
const tempPassword = randomBytes(16).toString('hex')
|
package/src/endpoints/login.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Endpoint } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from '../utils/slugs'
|
|
3
3
|
import { RateLimiter, type RateLimitStore } from '../utils/rateLimiter'
|
|
4
4
|
import { dbCreate } from '../utils/db'
|
|
5
|
+
import { issueTwoFactorChallenge } from '../utils/twoFactorChallenge'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -9,7 +10,7 @@ import { dbCreate } from '../utils/db'
|
|
|
9
10
|
* Client login endpoint.
|
|
10
11
|
*/
|
|
11
12
|
export function createLoginEndpoint(slugs: CollectionSlugs, store?: RateLimitStore): Endpoint {
|
|
12
|
-
const loginLimiter = new RateLimiter(15 * 60_000, 10, store)
|
|
13
|
+
const loginLimiter = new RateLimiter(15 * 60_000, 10, store, 'login')
|
|
13
14
|
return {
|
|
14
15
|
path: '/support/login',
|
|
15
16
|
method: 'post',
|
|
@@ -75,8 +76,20 @@ export function createLoginEndpoint(slugs: CollectionSlugs, store?: RateLimitSto
|
|
|
75
76
|
|
|
76
77
|
// 2FA gate: password was correct but a fresh 2FA verification is required.
|
|
77
78
|
// No session/cookie is issued (Payload rolled it back on the throw).
|
|
79
|
+
//
|
|
80
|
+
// The password check just succeeded, so this is where the short-lived
|
|
81
|
+
// proof for `POST /support/2fa {action:'send'}` is minted: without it,
|
|
82
|
+
// that endpoint would keep letting an anonymous caller burn a victim's
|
|
83
|
+
// send quota and lock them out of their own account.
|
|
78
84
|
if (errorMessage.includes('2FA_REQUIRED')) {
|
|
79
|
-
|
|
85
|
+
let challenge: string | undefined
|
|
86
|
+
try {
|
|
87
|
+
challenge = issueTwoFactorChallenge(email)
|
|
88
|
+
} catch {
|
|
89
|
+
// PAYLOAD_SECRET missing: 2FA cannot operate at all (codes are
|
|
90
|
+
// hashed with it). Answer without a challenge — fail closed.
|
|
91
|
+
}
|
|
92
|
+
return Response.json({ requires2FA: true, ...(challenge ? { challenge } : {}) }, { status: 200 })
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
let errorReason = 'Identifiants incorrects'
|