@promptbook/cli 0.112.0-139 → 0.112.0-140
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 +1 -1
- package/apps/agents-server/README.md +9 -0
- package/apps/agents-server/src/app/actions.ts +18 -7
- package/apps/agents-server/src/app/admin/login-methods/shibboleth/page.tsx +7 -17
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +395 -30
- package/apps/agents-server/src/app/api/agents/export/route.ts +28 -2
- package/apps/agents-server/src/app/api/auth/change-password/route.ts +53 -6
- package/apps/agents-server/src/app/api/auth/login/route.ts +17 -8
- package/apps/agents-server/src/app/api/chat/export/pdf/route.ts +167 -2
- package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +24 -2
- package/apps/agents-server/src/app/api/metadata/import/route.ts +2 -1
- package/apps/agents-server/src/app/api/users/[username]/route.ts +3 -2
- package/apps/agents-server/src/app/api/users/route.ts +5 -4
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +1 -13
- package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +3 -19
- package/apps/agents-server/src/components/Homepage/AgentsListListView.tsx +6 -0
- package/apps/agents-server/src/components/Homepage/AgentsListViewContent.tsx +6 -0
- package/apps/agents-server/src/components/Homepage/SortableFolderCard.tsx +7 -1
- package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +45 -43
- package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +1 -3
- package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +2 -10
- package/apps/agents-server/src/database/seedCoreAgents.ts +8 -7
- package/apps/agents-server/src/message-providers/email/sendgrid/verifySendgridInboundParseWebhook.ts +345 -0
- package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +15 -2
- package/apps/agents-server/src/utils/authenticateUser.ts +110 -3
- package/apps/agents-server/src/utils/authenticationAttemptRateLimit.ts +504 -0
- package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +76 -5
- package/apps/agents-server/src/utils/chatExport/renderHtmlToPdfOnServer.ts +32 -0
- package/apps/agents-server/src/utils/chatExport/sanitizeChatPdfExportHtml.ts +193 -0
- package/apps/agents-server/src/utils/currentUserIdentity.ts +22 -9
- package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +5 -0
- package/apps/agents-server/src/utils/getUserById.ts +19 -5
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +5 -0
- package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +32 -16
- package/apps/agents-server/src/utils/publicUser.ts +59 -0
- package/apps/agents-server/src/utils/shibbolethAuthentication.ts +17 -9
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobPersistenceController.ts +17 -11
- package/apps/agents-server/src/utils/userChat/createUserChatHarnessProgressCard.ts +99 -0
- package/apps/agents-server/src/utils/userChat/createUserChatRunnerProgressCard.ts +63 -0
- package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +0 -3
- package/apps/agents-server/src/utils/vpsConfiguration.ts +3 -0
- package/esm/index.es.js +14 -5
- package/esm/index.es.js.map +1 -1
- package/esm/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/book-components/Chat/Chat/createChatMessageToolCallRenderModel.ts +2 -9
- package/src/book-components/Chat/utils/isVisibleChatToolCall.ts +23 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +868 -755
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +14 -5
- package/umd/index.umd.js.map +1 -1
- package/umd/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
- package/umd/src/version.d.ts +1 -1
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server';
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { $getTableName } from '../../../../database/$getTableName';
|
|
3
3
|
import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
|
|
4
|
-
import { AgentsServerDatabase } from '../../../../database/schema';
|
|
4
|
+
import type { AgentsServerDatabase } from '../../../../database/schema';
|
|
5
5
|
import { getPasswordValidationMessage, hashPassword, verifyPassword } from '../../../../utils/auth';
|
|
6
|
+
import {
|
|
7
|
+
AUTHENTICATION_ATTEMPT_PURPOSES,
|
|
8
|
+
checkAuthenticationAttemptRateLimit,
|
|
9
|
+
createAuthenticationAttemptRateLimitResponse,
|
|
10
|
+
recordAuthenticationAttempt,
|
|
11
|
+
recordRateLimitedAuthenticationAttempt,
|
|
12
|
+
resolveAuthenticationAttemptRequestIp,
|
|
13
|
+
} from '../../../../utils/authenticationAttemptRateLimit';
|
|
6
14
|
import { getCurrentUser } from '../../../../utils/getCurrentUser';
|
|
7
15
|
|
|
16
|
+
/**
|
|
17
|
+
* User row shape needed to verify and update a password change.
|
|
18
|
+
*/
|
|
19
|
+
type ChangePasswordUserRow = Pick<AgentsServerDatabase['public']['Tables']['User']['Row'], 'id' | 'passwordHash'>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Supabase projection for user fields needed by password changes.
|
|
23
|
+
*/
|
|
24
|
+
const CHANGE_PASSWORD_USER_SELECT_COLUMNS = 'id, passwordHash';
|
|
25
|
+
|
|
8
26
|
/**
|
|
9
27
|
* Handles post.
|
|
10
28
|
*/
|
|
11
|
-
export async function POST(request:
|
|
29
|
+
export async function POST(request: NextRequest) {
|
|
12
30
|
try {
|
|
13
31
|
const user = await getCurrentUser();
|
|
14
32
|
if (!user) {
|
|
@@ -18,7 +36,12 @@ export async function POST(request: Request) {
|
|
|
18
36
|
const body = await request.json();
|
|
19
37
|
const { currentPassword, newPassword } = body;
|
|
20
38
|
|
|
21
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
typeof currentPassword !== 'string' ||
|
|
41
|
+
typeof newPassword !== 'string' ||
|
|
42
|
+
!currentPassword ||
|
|
43
|
+
!newPassword
|
|
44
|
+
) {
|
|
22
45
|
return NextResponse.json({ error: 'Missing password fields' }, { status: 400 });
|
|
23
46
|
}
|
|
24
47
|
|
|
@@ -34,10 +57,27 @@ export async function POST(request: Request) {
|
|
|
34
57
|
);
|
|
35
58
|
}
|
|
36
59
|
|
|
60
|
+
const requestIp = resolveAuthenticationAttemptRequestIp(request);
|
|
61
|
+
const rateLimitDecision = checkAuthenticationAttemptRateLimit({
|
|
62
|
+
requestIp,
|
|
63
|
+
username: user.username,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!rateLimitDecision.isAllowed) {
|
|
67
|
+
recordRateLimitedAuthenticationAttempt({
|
|
68
|
+
requestIp,
|
|
69
|
+
username: user.username,
|
|
70
|
+
purpose: AUTHENTICATION_ATTEMPT_PURPOSES.CHANGE_PASSWORD,
|
|
71
|
+
rejection: rateLimitDecision,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return createAuthenticationAttemptRateLimitResponse(rateLimitDecision);
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
const supabase = $provideSupabaseForServer();
|
|
38
78
|
const { data: userData, error: fetchError } = await supabase
|
|
39
79
|
.from(await $getTableName('User'))
|
|
40
|
-
.select(
|
|
80
|
+
.select(CHANGE_PASSWORD_USER_SELECT_COLUMNS)
|
|
41
81
|
.eq('username', user.username)
|
|
42
82
|
.single();
|
|
43
83
|
|
|
@@ -45,10 +85,17 @@ export async function POST(request: Request) {
|
|
|
45
85
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
46
86
|
}
|
|
47
87
|
|
|
48
|
-
const userRow = userData as
|
|
88
|
+
const userRow = userData as ChangePasswordUserRow;
|
|
49
89
|
|
|
50
90
|
// Verify current password
|
|
51
91
|
const isValid = await verifyPassword(currentPassword, userRow.passwordHash);
|
|
92
|
+
recordAuthenticationAttempt({
|
|
93
|
+
requestIp,
|
|
94
|
+
username: user.username,
|
|
95
|
+
purpose: AUTHENTICATION_ATTEMPT_PURPOSES.CHANGE_PASSWORD,
|
|
96
|
+
isSuccessful: isValid,
|
|
97
|
+
});
|
|
98
|
+
|
|
52
99
|
if (!isValid) {
|
|
53
100
|
return NextResponse.json({ error: 'Invalid current password' }, { status: 401 });
|
|
54
101
|
}
|
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { authenticateUserWithRateLimit } from '../../../../utils/authenticateUser';
|
|
2
|
+
import {
|
|
3
|
+
createAuthenticationAttemptRateLimitResponse,
|
|
4
|
+
resolveAuthenticationAttemptRequestIp,
|
|
5
|
+
} from '../../../../utils/authenticationAttemptRateLimit';
|
|
2
6
|
import { setSession } from '../../../../utils/session';
|
|
3
|
-
import { NextResponse } from 'next/server';
|
|
7
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Handles post.
|
|
7
11
|
*/
|
|
8
|
-
export async function POST(request:
|
|
12
|
+
export async function POST(request: NextRequest) {
|
|
9
13
|
try {
|
|
10
14
|
const body = await request.json();
|
|
11
15
|
const { username, password } = body;
|
|
12
16
|
|
|
13
|
-
if (!username || !password) {
|
|
17
|
+
if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) {
|
|
14
18
|
return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const
|
|
21
|
+
const authenticationResult = await authenticateUserWithRateLimit(username, password, {
|
|
22
|
+
requestIp: resolveAuthenticationAttemptRequestIp(request),
|
|
23
|
+
});
|
|
18
24
|
|
|
19
|
-
if (
|
|
20
|
-
|
|
25
|
+
if (authenticationResult.isRateLimited) {
|
|
26
|
+
return createAuthenticationAttemptRateLimitResponse(authenticationResult.rateLimitRejection);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (authenticationResult.user) {
|
|
30
|
+
await setSession(authenticationResult.user);
|
|
21
31
|
return NextResponse.json({ success: true });
|
|
22
32
|
} else {
|
|
23
33
|
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
24
34
|
}
|
|
25
|
-
|
|
26
35
|
} catch (error) {
|
|
27
36
|
console.error('Login error:', error);
|
|
28
37
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
@@ -3,13 +3,37 @@ import type { ChatMessage } from '../../../../../../../../src/book-components/Ch
|
|
|
3
3
|
import type { ChatParticipant } from '../../../../../../../../src/book-components/Chat/types/ChatParticipant';
|
|
4
4
|
import { createChatExportFilename } from '../../../../../../../../src/book-components/Chat/save/_common/createChatExportFilename';
|
|
5
5
|
import { NextRequest, NextResponse } from 'next/server';
|
|
6
|
+
import { spaceTrim } from 'spacetrim';
|
|
7
|
+
import { getCurrentUser, type UserInfo } from '@/src/utils/getCurrentUser';
|
|
6
8
|
import { renderHtmlToPdfOnServer } from '@/src/utils/chatExport/renderHtmlToPdfOnServer';
|
|
9
|
+
import { sanitizeChatPdfExportHtml } from '@/src/utils/chatExport/sanitizeChatPdfExportHtml';
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* PDF export requires the Node.js runtime because it depends on Playwright.
|
|
10
13
|
*/
|
|
11
14
|
export const runtime = 'nodejs';
|
|
12
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Maximum chat PDF exports one signed-in user can request per rate-limit window.
|
|
18
|
+
*
|
|
19
|
+
* @private internal constant for POST /api/chat/export/pdf
|
|
20
|
+
*/
|
|
21
|
+
const CHAT_PDF_EXPORT_MAX_REQUESTS_PER_WINDOW = 5;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sliding-window length for chat PDF export rate limiting.
|
|
25
|
+
*
|
|
26
|
+
* @private internal constant for POST /api/chat/export/pdf
|
|
27
|
+
*/
|
|
28
|
+
const CHAT_PDF_EXPORT_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* In-memory PDF export request timestamps keyed by authenticated user.
|
|
32
|
+
*
|
|
33
|
+
* @private internal constant for POST /api/chat/export/pdf
|
|
34
|
+
*/
|
|
35
|
+
const CHAT_PDF_EXPORT_RATE_LIMIT_BUCKETS: Map<string, Array<number>> = new Map();
|
|
36
|
+
|
|
13
37
|
/**
|
|
14
38
|
* Minimal request payload accepted by the chat PDF export endpoint.
|
|
15
39
|
*
|
|
@@ -21,10 +45,48 @@ type ChatPdfExportRequestBody = {
|
|
|
21
45
|
readonly participants: ReadonlyArray<ChatParticipant>;
|
|
22
46
|
};
|
|
23
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Result of the chat PDF export rate-limit check.
|
|
50
|
+
*
|
|
51
|
+
* @private internal type for POST /api/chat/export/pdf
|
|
52
|
+
*/
|
|
53
|
+
type ChatPdfExportRateLimitResult = {
|
|
54
|
+
readonly isAllowed: boolean;
|
|
55
|
+
readonly retryAfterSeconds: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
24
58
|
/**
|
|
25
59
|
* Builds a server-rendered PDF from the standalone HTML chat export.
|
|
26
60
|
*/
|
|
27
61
|
export async function POST(request: NextRequest) {
|
|
62
|
+
const currentUser = await getCurrentUser();
|
|
63
|
+
if (!currentUser) {
|
|
64
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rateLimitCheck = checkChatPdfExportRateLimit(currentUser);
|
|
68
|
+
if (!rateLimitCheck.isAllowed) {
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{
|
|
71
|
+
error: spaceTrim(`
|
|
72
|
+
Rate limit exceeded for \`PDF\` chat exports.
|
|
73
|
+
|
|
74
|
+
**Limit:** ${CHAT_PDF_EXPORT_MAX_REQUESTS_PER_WINDOW} requests per ${Math.round(
|
|
75
|
+
CHAT_PDF_EXPORT_RATE_LIMIT_WINDOW_MS / 1000,
|
|
76
|
+
)} seconds.
|
|
77
|
+
|
|
78
|
+
Try again in **${rateLimitCheck.retryAfterSeconds}** seconds.
|
|
79
|
+
`),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
status: 429,
|
|
83
|
+
headers: {
|
|
84
|
+
'Retry-After': String(rateLimitCheck.retryAfterSeconds),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
28
90
|
let requestBody: ChatPdfExportRequestBody;
|
|
29
91
|
|
|
30
92
|
try {
|
|
@@ -45,15 +107,16 @@ export async function POST(request: NextRequest) {
|
|
|
45
107
|
}
|
|
46
108
|
|
|
47
109
|
try {
|
|
48
|
-
const
|
|
110
|
+
const html = sanitizeChatPdfExportHtml(
|
|
49
111
|
buildChatHtml(requestBody.title, requestBody.messages, requestBody.participants),
|
|
50
112
|
);
|
|
113
|
+
const pdfBuffer = await renderHtmlToPdfOnServer(html);
|
|
51
114
|
const filename = createChatExportFilename(requestBody.title, 'pdf');
|
|
52
115
|
|
|
53
116
|
return new NextResponse(pdfBuffer, {
|
|
54
117
|
headers: {
|
|
55
118
|
'Content-Type': 'application/pdf',
|
|
56
|
-
'Content-Disposition':
|
|
119
|
+
'Content-Disposition': createChatPdfExportContentDisposition(filename),
|
|
57
120
|
},
|
|
58
121
|
});
|
|
59
122
|
} catch (error) {
|
|
@@ -61,3 +124,105 @@ export async function POST(request: NextRequest) {
|
|
|
61
124
|
return NextResponse.json({ error: 'Failed to export chat as PDF.' }, { status: 500 });
|
|
62
125
|
}
|
|
63
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Applies a per-user sliding-window rate limit to Playwright-backed PDF exports.
|
|
130
|
+
*
|
|
131
|
+
* @param currentUser - Authenticated caller resolved from the request.
|
|
132
|
+
* @returns Decision and retry timing for the current attempt.
|
|
133
|
+
*
|
|
134
|
+
* @private internal helper for POST /api/chat/export/pdf
|
|
135
|
+
*/
|
|
136
|
+
function checkChatPdfExportRateLimit(currentUser: UserInfo): ChatPdfExportRateLimitResult {
|
|
137
|
+
const bucketKey = resolveChatPdfExportRateLimitUserKey(currentUser);
|
|
138
|
+
const nowMs = Date.now();
|
|
139
|
+
const windowStartMs = nowMs - CHAT_PDF_EXPORT_RATE_LIMIT_WINDOW_MS;
|
|
140
|
+
const previousAttempts = CHAT_PDF_EXPORT_RATE_LIMIT_BUCKETS.get(bucketKey) || [];
|
|
141
|
+
const recentAttempts = previousAttempts.filter((attemptTimeMs) => attemptTimeMs > windowStartMs);
|
|
142
|
+
|
|
143
|
+
if (recentAttempts.length >= CHAT_PDF_EXPORT_MAX_REQUESTS_PER_WINDOW) {
|
|
144
|
+
const oldestRelevantAttemptMs = recentAttempts[0]!;
|
|
145
|
+
const retryAfterMs = Math.max(
|
|
146
|
+
1_000,
|
|
147
|
+
oldestRelevantAttemptMs + CHAT_PDF_EXPORT_RATE_LIMIT_WINDOW_MS - nowMs,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
CHAT_PDF_EXPORT_RATE_LIMIT_BUCKETS.set(bucketKey, recentAttempts);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
isAllowed: false,
|
|
154
|
+
retryAfterSeconds: Math.ceil(retryAfterMs / 1_000),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
recentAttempts.push(nowMs);
|
|
159
|
+
CHAT_PDF_EXPORT_RATE_LIMIT_BUCKETS.set(bucketKey, recentAttempts);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
isAllowed: true,
|
|
163
|
+
retryAfterSeconds: 0,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Resolves a stable rate-limit bucket key for one authenticated user.
|
|
169
|
+
*
|
|
170
|
+
* @param currentUser - Authenticated caller resolved from the request.
|
|
171
|
+
* @returns Per-user bucket key.
|
|
172
|
+
*
|
|
173
|
+
* @private internal helper for POST /api/chat/export/pdf
|
|
174
|
+
*/
|
|
175
|
+
function resolveChatPdfExportRateLimitUserKey(currentUser: UserInfo): string {
|
|
176
|
+
if (typeof currentUser.id === 'number' && Number.isFinite(currentUser.id)) {
|
|
177
|
+
return `user:${currentUser.id}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `username:${currentUser.username.trim().toLowerCase()}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds a standards-compatible attachment header with both quoted ASCII
|
|
185
|
+
* fallback and RFC 5987 UTF-8 filename.
|
|
186
|
+
*
|
|
187
|
+
* @param filename - Sanitized chat export filename.
|
|
188
|
+
* @returns Value for the `Content-Disposition` header.
|
|
189
|
+
*
|
|
190
|
+
* @private internal helper for POST /api/chat/export/pdf
|
|
191
|
+
*/
|
|
192
|
+
function createChatPdfExportContentDisposition(filename: string): string {
|
|
193
|
+
const fallbackFilename = createChatPdfExportFallbackFilename(filename);
|
|
194
|
+
const encodedFilename = encodeRFC5987Value(filename);
|
|
195
|
+
|
|
196
|
+
return `attachment; filename="${fallbackFilename}"; filename*=UTF-8''${encodedFilename}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Creates a quoted-string-safe ASCII fallback filename.
|
|
201
|
+
*
|
|
202
|
+
* @param filename - Preferred filename.
|
|
203
|
+
* @returns Filename safe for a quoted `Content-Disposition` parameter.
|
|
204
|
+
*
|
|
205
|
+
* @private internal helper for POST /api/chat/export/pdf
|
|
206
|
+
*/
|
|
207
|
+
function createChatPdfExportFallbackFilename(filename: string): string {
|
|
208
|
+
const fallbackFilename = filename
|
|
209
|
+
.replace(/[^\x20-\x7e]/g, '_')
|
|
210
|
+
.replace(/[\r\n"\\/;]/g, '_')
|
|
211
|
+
.trim();
|
|
212
|
+
|
|
213
|
+
return fallbackFilename || 'chat-export.pdf';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Encodes a `Content-Disposition` filename according to RFC 5987.
|
|
218
|
+
*
|
|
219
|
+
* @param value - Raw filename value.
|
|
220
|
+
* @returns Percent-encoded filename.
|
|
221
|
+
*
|
|
222
|
+
* @private internal helper for POST /api/chat/export/pdf
|
|
223
|
+
*/
|
|
224
|
+
function encodeRFC5987Value(value: string): string {
|
|
225
|
+
return encodeURIComponent(value).replace(/['()*]/g, (character) =>
|
|
226
|
+
`%${character.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { $getTableName } from '../../../../../database/$getTableName';
|
|
2
2
|
import { $provideSupabaseForServer } from '../../../../../database/$provideSupabaseForServer';
|
|
3
3
|
import { parseInboundSendgridEmail } from '../../../../../message-providers/email/sendgrid/parseInboundSendgridEmail';
|
|
4
|
+
import {
|
|
5
|
+
resolveSendgridInboundParseWebhookErrorStatus,
|
|
6
|
+
verifySendgridInboundParseWebhook,
|
|
7
|
+
} from '../../../../../message-providers/email/sendgrid/verifySendgridInboundParseWebhook';
|
|
4
8
|
import { NextRequest, NextResponse } from 'next/server';
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -8,7 +12,10 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
8
12
|
*/
|
|
9
13
|
export async function POST(request: NextRequest) {
|
|
10
14
|
try {
|
|
11
|
-
const
|
|
15
|
+
const rawBody = Buffer.from(await request.arrayBuffer());
|
|
16
|
+
verifySendgridInboundParseWebhook(request.headers, rawBody);
|
|
17
|
+
|
|
18
|
+
const formData = await createRequestFromRawBody(request, rawBody).formData();
|
|
12
19
|
const rawEmail = formData.get('email');
|
|
13
20
|
|
|
14
21
|
if (typeof rawEmail !== 'string') {
|
|
@@ -45,7 +52,22 @@ export async function POST(request: NextRequest) {
|
|
|
45
52
|
console.error('Error processing inbound email', error);
|
|
46
53
|
return NextResponse.json(
|
|
47
54
|
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
48
|
-
{ status:
|
|
55
|
+
{ status: resolveSendgridInboundParseWebhookErrorStatus(error) },
|
|
49
56
|
);
|
|
50
57
|
}
|
|
51
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Recreates a request from verified raw bytes so `formData()` can parse the multipart payload.
|
|
62
|
+
*
|
|
63
|
+
* @param request - Original incoming request.
|
|
64
|
+
* @param rawBody - Verified raw request body.
|
|
65
|
+
* @returns Request with an unread body stream.
|
|
66
|
+
*/
|
|
67
|
+
function createRequestFromRawBody(request: NextRequest, rawBody: Buffer): Request {
|
|
68
|
+
return new Request(request.url, {
|
|
69
|
+
method: request.method,
|
|
70
|
+
headers: request.headers,
|
|
71
|
+
body: new Uint8Array(rawBody),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -28,7 +28,8 @@ export async function POST(request: NextRequest) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
|
-
const
|
|
31
|
+
const isDefaultResetSkipped = request.nextUrl.searchParams.get('isDefaultResetSkipped') === 'true';
|
|
32
|
+
const importSummary = await importMetadataConfigurationPayload(payload, { isDefaultResetSkipped });
|
|
32
33
|
return NextResponse.json(importSummary);
|
|
33
34
|
} catch (error) {
|
|
34
35
|
if (error instanceof ParseError) {
|
|
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
|
|
3
3
|
import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
|
|
4
4
|
import { getPasswordValidationMessage, hashPassword } from '../../../../utils/auth';
|
|
5
5
|
import { isUserAdmin } from '../../../../utils/isUserAdmin';
|
|
6
|
+
import { PUBLIC_USER_SELECT_COLUMNS, toPublicUser, type PublicUser } from '../../../../utils/publicUser';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Handles patch.
|
|
@@ -34,7 +35,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ us
|
|
|
34
35
|
.from(await $getTableName('User'))
|
|
35
36
|
.update(updates)
|
|
36
37
|
.eq('username', usernameParam)
|
|
37
|
-
.select(
|
|
38
|
+
.select(PUBLIC_USER_SELECT_COLUMNS)
|
|
38
39
|
.single();
|
|
39
40
|
|
|
40
41
|
if (error) {
|
|
@@ -45,7 +46,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ us
|
|
|
45
46
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
return NextResponse.json(updatedUser);
|
|
49
|
+
return NextResponse.json(toPublicUser(updatedUser as unknown as PublicUser));
|
|
49
50
|
} catch (error) {
|
|
50
51
|
console.error('Update user error:', error);
|
|
51
52
|
const passwordValidationMessage = getPasswordValidationMessage(error);
|
|
@@ -2,6 +2,7 @@ import { $getTableName } from '@/src/database/$getTableName';
|
|
|
2
2
|
import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
|
|
3
3
|
import { getPasswordValidationMessage, hashPassword } from '../../../utils/auth';
|
|
4
4
|
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
5
|
+
import { PUBLIC_USER_SELECT_COLUMNS, toPublicUser, type PublicUser } from '../../../utils/publicUser';
|
|
5
6
|
import { NextResponse } from 'next/server';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -16,14 +17,14 @@ export async function GET() {
|
|
|
16
17
|
const supabase = $provideSupabaseForServer();
|
|
17
18
|
const { data: users, error } = await supabase
|
|
18
19
|
.from(await $getTableName('User'))
|
|
19
|
-
.select(
|
|
20
|
+
.select(PUBLIC_USER_SELECT_COLUMNS)
|
|
20
21
|
.order('username');
|
|
21
22
|
|
|
22
23
|
if (error) {
|
|
23
24
|
throw error;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
return NextResponse.json(users);
|
|
27
|
+
return NextResponse.json(((users || []) as unknown as PublicUser[]).map(toPublicUser));
|
|
27
28
|
} catch (error) {
|
|
28
29
|
console.error('List users error:', error);
|
|
29
30
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
@@ -58,7 +59,7 @@ export async function POST(request: Request) {
|
|
|
58
59
|
createdAt: new Date().toISOString(),
|
|
59
60
|
updatedAt: new Date().toISOString(),
|
|
60
61
|
})
|
|
61
|
-
.select(
|
|
62
|
+
.select(PUBLIC_USER_SELECT_COLUMNS)
|
|
62
63
|
.single();
|
|
63
64
|
|
|
64
65
|
if (error) {
|
|
@@ -69,7 +70,7 @@ export async function POST(request: Request) {
|
|
|
69
70
|
throw error;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
return NextResponse.json(newUser);
|
|
73
|
+
return NextResponse.json(toPublicUser(newUser as unknown as PublicUser));
|
|
73
74
|
} catch (error) {
|
|
74
75
|
console.error('Create user error:', error);
|
|
75
76
|
const passwordValidationMessage = getPasswordValidationMessage(error);
|
|
@@ -6,7 +6,6 @@ import type { AgentOrganizationAgent, AgentOrganizationFolder } from '../../util
|
|
|
6
6
|
import { AgentsListDialogs } from './AgentsListDialogs';
|
|
7
7
|
import { AgentsListHeader } from './AgentsListHeader';
|
|
8
8
|
import { AgentsListViewContent } from './AgentsListViewContent';
|
|
9
|
-
import { AGENTS_IMPORT_FILE_INPUT_ACCEPT } from './useAgentsListImportExportState';
|
|
10
9
|
import type { AgentWithVisibility } from './useFederatedAgents';
|
|
11
10
|
import { useAgentsListState } from './useAgentsListState';
|
|
12
11
|
|
|
@@ -82,17 +81,6 @@ export function AgentsList(props: AgentsListProps) {
|
|
|
82
81
|
onDragOver={isAdmin ? state.handleAgentsFileDragOver : undefined}
|
|
83
82
|
onDrop={isAdmin ? state.handleAgentsFileDrop : undefined}
|
|
84
83
|
>
|
|
85
|
-
{isAdmin && (
|
|
86
|
-
<input
|
|
87
|
-
ref={state.importFileInputRef}
|
|
88
|
-
type="file"
|
|
89
|
-
multiple
|
|
90
|
-
accept={AGENTS_IMPORT_FILE_INPUT_ACCEPT}
|
|
91
|
-
className="hidden"
|
|
92
|
-
onChange={(event) => void state.handleAgentsImportFileChange(event)}
|
|
93
|
-
disabled={state.isAgentsImporting}
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
96
84
|
{isAdmin && state.isAgentsImportDragActive && (
|
|
97
85
|
<div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center rounded-xl border-2 border-dashed border-blue-400 bg-blue-50/85 text-sm font-medium text-blue-800 shadow-sm">
|
|
98
86
|
Drop books or ZIP archives to import agents
|
|
@@ -109,7 +97,6 @@ export function AgentsList(props: AgentsListProps) {
|
|
|
109
97
|
isAgentsImporting={state.isAgentsImporting}
|
|
110
98
|
onCreateFolder={state.handleCreateFolder}
|
|
111
99
|
onExportAgents={() => void state.handleAgentsExport()}
|
|
112
|
-
onImportAgents={state.handleAgentsImportClick}
|
|
113
100
|
onNavigateToFolder={state.navigateToFolder}
|
|
114
101
|
onSetViewMode={state.setViewMode}
|
|
115
102
|
viewMode={state.viewMode}
|
|
@@ -138,6 +125,7 @@ export function AgentsList(props: AgentsListProps) {
|
|
|
138
125
|
handleDragEnd={state.handleDragEnd}
|
|
139
126
|
handleDragOver={state.handleDragOver}
|
|
140
127
|
handleDragStart={state.handleDragStart}
|
|
128
|
+
handleAgentsFileDropIntoFolder={state.handleAgentsFileDropIntoFolder}
|
|
141
129
|
handleFolderContextMenu={state.handleFolderContextMenu}
|
|
142
130
|
handleRenameFolder={state.handleRenameFolder}
|
|
143
131
|
handleRequestAgentVisibilityChange={state.handleRequestAgentVisibilityChange}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
Grid,
|
|
9
9
|
Map,
|
|
10
10
|
Network,
|
|
11
|
-
UploadIcon,
|
|
12
11
|
type LucideIcon,
|
|
13
12
|
} from 'lucide-react';
|
|
14
13
|
import type { AgentOrganizationFolder } from '../../utils/agentOrganization/types';
|
|
@@ -70,13 +69,9 @@ type AgentsListHeaderProps = {
|
|
|
70
69
|
*/
|
|
71
70
|
readonly isAdmin: boolean;
|
|
72
71
|
/**
|
|
73
|
-
* Downloads an agents
|
|
72
|
+
* Downloads an agents archive.
|
|
74
73
|
*/
|
|
75
74
|
readonly onExportAgents: () => void;
|
|
76
|
-
/**
|
|
77
|
-
* Opens the agents import file picker.
|
|
78
|
-
*/
|
|
79
|
-
readonly onImportAgents: () => void;
|
|
80
75
|
/**
|
|
81
76
|
* Opens the create-folder dialog.
|
|
82
77
|
*/
|
|
@@ -135,7 +130,6 @@ export function AgentsListHeader({
|
|
|
135
130
|
isAgentsImporting,
|
|
136
131
|
onCreateFolder,
|
|
137
132
|
onExportAgents,
|
|
138
|
-
onImportAgents,
|
|
139
133
|
onNavigateToFolder,
|
|
140
134
|
onSetViewMode,
|
|
141
135
|
viewMode,
|
|
@@ -180,20 +174,10 @@ export function AgentsListHeader({
|
|
|
180
174
|
onClick={onExportAgents}
|
|
181
175
|
disabled={isAgentsExporting || isAgentsImporting}
|
|
182
176
|
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-sm disabled:cursor-not-allowed disabled:opacity-60"
|
|
183
|
-
title="
|
|
177
|
+
title="Download agents"
|
|
184
178
|
>
|
|
185
179
|
<DownloadIcon className="w-4 h-4" />
|
|
186
|
-
{isAgentsExporting ? '
|
|
187
|
-
</button>
|
|
188
|
-
<button
|
|
189
|
-
type="button"
|
|
190
|
-
onClick={onImportAgents}
|
|
191
|
-
disabled={isAgentsExporting || isAgentsImporting}
|
|
192
|
-
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-sm disabled:cursor-not-allowed disabled:opacity-60"
|
|
193
|
-
title="Import agents"
|
|
194
|
-
>
|
|
195
|
-
<UploadIcon className="w-4 h-4" />
|
|
196
|
-
{isAgentsImporting ? 'Importing...' : 'Import'}
|
|
180
|
+
{isAgentsExporting ? 'Downloading...' : 'Download'}
|
|
197
181
|
</button>
|
|
198
182
|
</>
|
|
199
183
|
)}
|
|
@@ -101,6 +101,10 @@ type AgentsListListViewProps = {
|
|
|
101
101
|
* dnd-kit drag-start handler.
|
|
102
102
|
*/
|
|
103
103
|
readonly handleDragStart: ComponentProps<typeof DndContext>['onDragStart'];
|
|
104
|
+
/**
|
|
105
|
+
* Imports dropped book files or ZIP archives directly into one folder.
|
|
106
|
+
*/
|
|
107
|
+
readonly handleAgentsFileDropIntoFolder: SortableFolderCardProps['onFileDrop'];
|
|
104
108
|
/**
|
|
105
109
|
* Handles folder context-menu requests.
|
|
106
110
|
*/
|
|
@@ -193,6 +197,7 @@ export function AgentsListListView({
|
|
|
193
197
|
handleDragEnd,
|
|
194
198
|
handleDragOver,
|
|
195
199
|
handleDragStart,
|
|
200
|
+
handleAgentsFileDropIntoFolder,
|
|
196
201
|
handleFolderContextMenu,
|
|
197
202
|
handleRenameFolder,
|
|
198
203
|
handleRequestAgentVisibilityChange,
|
|
@@ -239,6 +244,7 @@ export function AgentsListListView({
|
|
|
239
244
|
onRename={canOrganize ? () => handleRenameFolder(folder.id) : undefined}
|
|
240
245
|
onDelete={canOrganize ? () => handleDeleteFolder(folder.id) : undefined}
|
|
241
246
|
onContextMenu={handleFolderContextMenu}
|
|
247
|
+
onFileDrop={isAdmin ? handleAgentsFileDropIntoFolder : undefined}
|
|
242
248
|
dragHandleLabel={dragFolderLabel}
|
|
243
249
|
allowFullCardDrag={allowFullCardDrag}
|
|
244
250
|
/>
|
|
@@ -111,6 +111,10 @@ type AgentsListViewContentProps = {
|
|
|
111
111
|
* dnd-kit drag-start handler.
|
|
112
112
|
*/
|
|
113
113
|
readonly handleDragStart: ComponentProps<typeof DndContext>['onDragStart'];
|
|
114
|
+
/**
|
|
115
|
+
* Imports dropped book files or ZIP archives directly into one folder.
|
|
116
|
+
*/
|
|
117
|
+
readonly handleAgentsFileDropIntoFolder: ComponentProps<typeof AgentsListListView>['handleAgentsFileDropIntoFolder'];
|
|
114
118
|
/**
|
|
115
119
|
* Handles folder context-menu requests.
|
|
116
120
|
*/
|
|
@@ -269,6 +273,7 @@ export function AgentsListViewContent({
|
|
|
269
273
|
handleDragEnd,
|
|
270
274
|
handleDragOver,
|
|
271
275
|
handleDragStart,
|
|
276
|
+
handleAgentsFileDropIntoFolder,
|
|
272
277
|
handleFolderContextMenu,
|
|
273
278
|
handleRenameFolder,
|
|
274
279
|
handleRequestAgentVisibilityChange,
|
|
@@ -308,6 +313,7 @@ export function AgentsListViewContent({
|
|
|
308
313
|
handleDragEnd={handleDragEnd}
|
|
309
314
|
handleDragOver={handleDragOver}
|
|
310
315
|
handleDragStart={handleDragStart}
|
|
316
|
+
handleAgentsFileDropIntoFolder={handleAgentsFileDropIntoFolder}
|
|
311
317
|
handleFolderContextMenu={handleFolderContextMenu}
|
|
312
318
|
handleRenameFolder={handleRenameFolder}
|
|
313
319
|
handleRequestAgentVisibilityChange={handleRequestAgentVisibilityChange}
|
|
@@ -4,7 +4,7 @@ import { CSS } from '@dnd-kit/utilities';
|
|
|
4
4
|
import { useSortable } from '@dnd-kit/sortable';
|
|
5
5
|
import type { string_url } from '@promptbook-local/types';
|
|
6
6
|
import type { AgentBasicInformation } from '../../../../../src/book-2.0/agent-source/AgentBasicInformation';
|
|
7
|
-
import type { CSSProperties, MouseEvent } from 'react';
|
|
7
|
+
import type { CSSProperties, DragEvent, MouseEvent } from 'react';
|
|
8
8
|
import type { AgentOrganizationFolder } from '../../utils/agentOrganization/types';
|
|
9
9
|
import { FolderCard } from './FolderCard';
|
|
10
10
|
import { buildCardDragProps, DragHandle } from './DragHandle';
|
|
@@ -61,6 +61,10 @@ export type SortableFolderCardProps = {
|
|
|
61
61
|
* Context menu handler for the folder.
|
|
62
62
|
*/
|
|
63
63
|
readonly onContextMenu?: (event: MouseEvent<HTMLDivElement>, folder: AgentOrganizationFolder) => void;
|
|
64
|
+
/**
|
|
65
|
+
* File-drop handler for importing agents directly into this folder.
|
|
66
|
+
*/
|
|
67
|
+
readonly onFileDrop?: (event: DragEvent<HTMLDivElement>, folderId: number) => void;
|
|
64
68
|
/**
|
|
65
69
|
* Accessible label displayed for the drag handle.
|
|
66
70
|
*/
|
|
@@ -88,6 +92,7 @@ export function SortableFolderCard({
|
|
|
88
92
|
onRename,
|
|
89
93
|
onDelete,
|
|
90
94
|
onContextMenu,
|
|
95
|
+
onFileDrop,
|
|
91
96
|
dragHandleLabel,
|
|
92
97
|
allowFullCardDrag,
|
|
93
98
|
}: SortableFolderCardProps) {
|
|
@@ -125,6 +130,7 @@ export function SortableFolderCard({
|
|
|
125
130
|
className={`relative ${canOrganize ? 'select-none' : ''} ${isDragging ? 'opacity-0' : ''} ${dropClasses}`}
|
|
126
131
|
{...dragProps}
|
|
127
132
|
onContextMenu={(event) => onContextMenu?.(event, folder)}
|
|
133
|
+
onDrop={(event) => onFileDrop?.(event, folder.id)}
|
|
128
134
|
>
|
|
129
135
|
<FolderCard
|
|
130
136
|
folderName={folder.name}
|