mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(security): harden file access controls, webhook auth, and input bounds (#4601)
* fix(security): harden file access controls, webhook auth, and input bounds * fix(security): extend file access checks to remaining tool routes * fix(logs): address PR review comments on time filter * fix(logs): set end-time milliseconds to 999 for datetime filter strings * fix(files): return 404 instead of 500 on file access denial in utility paths * remove tooltip from resource tabs
This commit is contained in:
@@ -14,6 +14,14 @@ import { isUuid } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('FileAuthorization')
|
||||
|
||||
/** Thrown by utility functions when file access is denied, so route handlers can return 404. */
|
||||
export class FileAccessDeniedError extends Error {
|
||||
constructor() {
|
||||
super('File not found')
|
||||
this.name = 'FileAccessDeniedError'
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthorizationResult {
|
||||
granted: boolean
|
||||
reason: string
|
||||
@@ -598,7 +606,7 @@ async function authorizeFileAccess(
|
||||
*/
|
||||
export async function assertToolFileAccess(
|
||||
key: unknown,
|
||||
userId: string | undefined,
|
||||
userId: string,
|
||||
requestId: string,
|
||||
routeLogger: ReturnType<typeof createLogger>
|
||||
): Promise<NextResponse | null> {
|
||||
@@ -606,10 +614,6 @@ export async function assertToolFileAccess(
|
||||
routeLogger.warn(`[${requestId}] File access check rejected: missing key`)
|
||||
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
}
|
||||
if (!userId) {
|
||||
routeLogger.warn(`[${requestId}] File access check requires userId but none available`)
|
||||
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
}
|
||||
const hasAccess = await verifyFileAccess(key, userId)
|
||||
if (!hasAccess) {
|
||||
routeLogger.warn(`[${requestId}] File access denied for user`, { userId, key })
|
||||
|
||||
@@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -22,7 +23,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
@@ -66,6 +67,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
`[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)`
|
||||
)
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
const resolvedFileName = data.fileName || userFile.name || 'attachment'
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
@@ -49,6 +50,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const userFile = userFiles[0]
|
||||
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
fileName = validatedData.fileName || userFile.name
|
||||
} else if (validatedData.fileContent) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processSingleFileToUserFile, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { getConfluenceCloudId } from '@/tools/confluence/utils'
|
||||
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
|
||||
@@ -80,6 +81,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(
|
||||
userFile.key,
|
||||
auth.userId,
|
||||
'confluence-upload',
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
|
||||
let fileBuffer: Buffer
|
||||
try {
|
||||
fileBuffer = await downloadFileFromStorage(userFile, 'confluence-upload', logger)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -30,8 +31,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Discord send request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(discordSendMessageContract, request, {})
|
||||
@@ -134,17 +136,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
formData.append('payload_json', JSON.stringify(payload))
|
||||
|
||||
const downloadedFiles = await Promise.all(
|
||||
userFiles.map(async (userFile, i) => {
|
||||
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
|
||||
return { userFile, buffer }
|
||||
const accessResults = await Promise.all(
|
||||
userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
userFiles.map(async (file, i) => {
|
||||
try {
|
||||
logger.info(`[${requestId}] Downloading file ${i}: ${file.name}`)
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
for (let i = 0; i < downloadedFiles.length; i++) {
|
||||
const { userFile, buffer } = downloadedFiles[i]
|
||||
for (let i = 0; i < userFiles.length; i++) {
|
||||
const userFile = userFiles[i]
|
||||
const buffer = buffers[i]
|
||||
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
|
||||
filesOutput.push({
|
||||
name: userFile.name,
|
||||
mimeType: userFile.type || 'application/octet-stream',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
const logger = createLogger('DocuSignAPI')
|
||||
|
||||
@@ -54,7 +55,7 @@ async function resolveAccount(accessToken: string): Promise<DocuSignAccountInfo>
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
switch (operation) {
|
||||
case 'send_envelope':
|
||||
return await handleSendEnvelope(apiBase, headers, params)
|
||||
return await handleSendEnvelope(apiBase, headers, params, authResult.userId)
|
||||
case 'create_from_template':
|
||||
return await handleCreateFromTemplate(apiBase, headers, params)
|
||||
case 'get_envelope':
|
||||
@@ -115,7 +116,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
async function handleSendEnvelope(
|
||||
apiBase: string,
|
||||
headers: Record<string, string>,
|
||||
params: Record<string, unknown>
|
||||
params: Record<string, unknown>,
|
||||
userId: string
|
||||
) {
|
||||
const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params
|
||||
|
||||
@@ -135,6 +137,8 @@ async function handleSendEnvelope(
|
||||
const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger)
|
||||
if (userFiles.length > 0) {
|
||||
const userFile = userFiles[0]
|
||||
const denied = await assertToolFileAccess(userFile.key, userId, 'docusign-send', logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFile, 'docusign-send', logger)
|
||||
documentBase64 = buffer.toString('base64')
|
||||
documentName = userFile.name
|
||||
|
||||
@@ -8,6 +8,7 @@ import { httpHeaderSafeJson } from '@/lib/core/utils/validation'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
@@ -52,6 +53,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const userFile = userFiles[0]
|
||||
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
fileName = userFile.name
|
||||
} else if (validatedData.fileContent) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -43,6 +44,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
size: userFile.size,
|
||||
})
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import {
|
||||
base64UrlEncode,
|
||||
buildMimeMessage,
|
||||
@@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Gmail draft request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(gmailDraftContract, request, {})
|
||||
@@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentBuffers = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
|
||||
return {
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffer,
|
||||
}
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
@@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentBuffers = attachments.map((file, i) => ({
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffers[i],
|
||||
}))
|
||||
|
||||
const mimeMessage = buildMimeMessage({
|
||||
to: validatedData.to,
|
||||
cc: validatedData.cc ?? undefined,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import {
|
||||
base64UrlEncode,
|
||||
buildMimeMessage,
|
||||
@@ -25,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail edit draft attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -36,9 +37,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Gmail edit draft request via ${authResult.authType}`,
|
||||
{ userId: authResult.userId }
|
||||
{ userId }
|
||||
)
|
||||
|
||||
const parsed = await parseRequest(gmailEditDraftContract, request, {})
|
||||
@@ -81,17 +83,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentBuffers = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
return {
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffer,
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentBuffers = attachments.map((file, i) => ({
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffers[i],
|
||||
}))
|
||||
|
||||
const mimeMessage = buildMimeMessage({
|
||||
to: validatedData.to,
|
||||
cc: validatedData.cc ?? undefined,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import {
|
||||
base64UrlEncode,
|
||||
buildMimeMessage,
|
||||
@@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Gmail send request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(gmailSendContract, request, {})
|
||||
@@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentBuffers = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
|
||||
return {
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffer,
|
||||
}
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
@@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentBuffers = attachments.map((file, i) => ({
|
||||
filename: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
content: buffers[i],
|
||||
}))
|
||||
|
||||
const mimeMessage = buildMimeMessage({
|
||||
to: validatedData.to,
|
||||
cc: validatedData.cc ?? undefined,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import {
|
||||
GOOGLE_WORKSPACE_MIME_TYPES,
|
||||
handleSheetsFormat,
|
||||
@@ -52,7 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -113,6 +114,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
size: userFile.size,
|
||||
})
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
let fileBuffer: Buffer
|
||||
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
|
||||
|
||||
const logger = createLogger('JiraAddAttachmentAPI')
|
||||
@@ -17,7 +18,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
@@ -43,6 +44,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const formData = new FormData()
|
||||
|
||||
for (const file of userFiles) {
|
||||
const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
const blob = new Blob([new Uint8Array(buffer)], {
|
||||
type: file.type || 'application/octet-stream',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Dataverse upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
@@ -66,6 +67,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
} else if (validatedData.fileContent) {
|
||||
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
|
||||
|
||||
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { FileAccessDeniedError } from '@/app/api/files/authorization'
|
||||
import { uploadFilesForTeamsMessage } from '@/tools/microsoft_teams/server-utils'
|
||||
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
|
||||
import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils'
|
||||
@@ -20,7 +21,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -31,10 +32,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Teams channel write request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,6 +56,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
accessToken: validatedData.accessToken,
|
||||
requestId,
|
||||
logger,
|
||||
userId,
|
||||
})
|
||||
|
||||
let messageContent = validatedData.content
|
||||
@@ -160,6 +163,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof FileAccessDeniedError) {
|
||||
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
}
|
||||
logger.error(`[${requestId}] Error sending Teams channel message:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { FileAccessDeniedError } from '@/app/api/files/authorization'
|
||||
import { uploadFilesForTeamsMessage } from '@/tools/microsoft_teams/server-utils'
|
||||
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
|
||||
import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils'
|
||||
@@ -20,7 +21,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -31,10 +32,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Teams chat write request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -53,6 +55,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
accessToken: validatedData.accessToken,
|
||||
requestId,
|
||||
logger,
|
||||
userId,
|
||||
})
|
||||
|
||||
let messageContent = validatedData.content
|
||||
@@ -157,6 +160,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof FileAccessDeniedError) {
|
||||
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
}
|
||||
logger.error(`[${requestId}] Error sending Teams chat message:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
downloadFileFromStorage,
|
||||
resolveInternalFileUrl,
|
||||
} from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -120,6 +121,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
let base64 = userFile.base64
|
||||
if (!base64) {
|
||||
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
base64 = buffer.toString('base64')
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
processSingleFileToUserFile,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { normalizeExcelValues } from '@/tools/onedrive/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -47,7 +48,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -108,6 +109,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
try {
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
} catch (error) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Outlook draft request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(outlookDraftContract, request, {})
|
||||
@@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentObjects = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
|
||||
const base64Content = buffer.toString('base64')
|
||||
|
||||
return {
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
name: file.name,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
contentBytes: base64Content,
|
||||
}
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
@@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentObjects = attachments.map((file, i) => ({
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
name: file.name,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
contentBytes: buffers[i].toString('base64'),
|
||||
}))
|
||||
|
||||
logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`)
|
||||
message.attachments = attachmentObjects
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Outlook send request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(outlookSendContract, request, {})
|
||||
@@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentObjects = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
|
||||
const base64Content = buffer.toString('base64')
|
||||
|
||||
return {
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
name: file.name,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
contentBytes: base64Content,
|
||||
}
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
@@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentObjects = attachments.map((file, i) => ({
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
name: file.name,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
contentBytes: buffers[i].toString('base64'),
|
||||
}))
|
||||
|
||||
logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`)
|
||||
message.attachments = attachmentObjects
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
const logger = createLogger('QuiverImageToSvgAPI')
|
||||
|
||||
@@ -15,7 +16,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -47,6 +48,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const userFiles = processFilesToUserFiles([parsed as RawFileInput], requestId, logger)
|
||||
if (userFiles.length > 0) {
|
||||
const denied = await assertToolFileAccess(
|
||||
userFiles[0].key,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
|
||||
apiImage = { base64: buffer.toString('base64') }
|
||||
} else {
|
||||
@@ -64,6 +72,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
} else if (typeof data.image === 'object' && data.image !== null) {
|
||||
const userFiles = processFilesToUserFiles([data.image as RawFileInput], requestId, logger)
|
||||
if (userFiles.length > 0) {
|
||||
const denied = await assertToolFileAccess(
|
||||
userFiles[0].key,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
|
||||
apiImage = { base64: buffer.toString('base64') }
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
const logger = createLogger('QuiverTextToSvgAPI')
|
||||
|
||||
@@ -15,9 +16,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
const userId = authResult.userId
|
||||
|
||||
try {
|
||||
const parsed = await parseRequest(
|
||||
@@ -51,6 +53,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const userFiles = processFilesToUserFiles([parsed as RawFileInput], requestId, logger)
|
||||
if (userFiles.length > 0) {
|
||||
const denied = await assertToolFileAccess(
|
||||
userFiles[0].key,
|
||||
userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
|
||||
apiReferences.push({ base64: buffer.toString('base64') })
|
||||
}
|
||||
@@ -61,6 +70,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
} else if (typeof ref === 'object' && ref !== null) {
|
||||
const userFiles = processFilesToUserFiles([ref as RawFileInput], requestId, logger)
|
||||
if (userFiles.length > 0) {
|
||||
const denied = await assertToolFileAccess(userFiles[0].key, userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
|
||||
apiReferences.push({ base64: buffer.toString('base64') })
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -76,6 +77,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
uploadBody = buffer
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import {
|
||||
assertSafeExternalUrl,
|
||||
extractSapConcurError,
|
||||
@@ -180,13 +181,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Concur upload request: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
const userId = authResult.userId
|
||||
|
||||
// boundary-raw-json: internal upload envelope validated by SapConcurUploadRequestSchema below; not a public boundary
|
||||
const json = await request.json()
|
||||
@@ -204,6 +206,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
const userFile = userFiles[0]
|
||||
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
const fileName = userFile.name
|
||||
const mimeType = inferMimeType(fileName, userFile.type)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SendGrid send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
@@ -26,6 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated SendGrid send request via ${authResult.authType}`)
|
||||
|
||||
const parsed = await parseRequest(sendGridSendMailContract, request, {})
|
||||
@@ -97,20 +99,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const userFiles = processFilesToUserFiles(rawAttachments, requestId, logger)
|
||||
|
||||
if (userFiles.length > 0) {
|
||||
const sendGridAttachments = await Promise.all(
|
||||
const accessResults = await Promise.all(
|
||||
userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
userFiles.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
|
||||
return {
|
||||
content: buffer.toString('base64'),
|
||||
filename: file.name,
|
||||
type: file.type || 'application/octet-stream',
|
||||
disposition: 'attachment',
|
||||
}
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
@@ -120,6 +121,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
)
|
||||
|
||||
const sendGridAttachments = userFiles.map((file, i) => ({
|
||||
content: buffers[i].toString('base64'),
|
||||
filename: file.name,
|
||||
type: file.type || 'application/octet-stream',
|
||||
disposition: 'attachment',
|
||||
}))
|
||||
|
||||
mailBody.attachments = sendGridAttachments
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
|
||||
@@ -24,7 +24,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -5,7 +5,8 @@ import { parseRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { sendSlackMessage } from '../utils'
|
||||
import { FileAccessDeniedError } from '@/app/api/files/authorization'
|
||||
import { sendSlackMessage } from '@/app/api/tools/slack/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -17,7 +18,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -28,8 +29,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated Slack send request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(slackSendMessageContract, request, {})
|
||||
@@ -50,6 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
accessToken: validatedData.accessToken,
|
||||
channel: validatedData.channel ?? undefined,
|
||||
userId: validatedData.userId ?? undefined,
|
||||
ownerUserId: userId,
|
||||
text: validatedData.text,
|
||||
threadTs: validatedData.thread_ts ?? undefined,
|
||||
blocks: validatedData.blocks ?? undefined,
|
||||
@@ -65,6 +68,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
return NextResponse.json({ success: true, output: result.output })
|
||||
} catch (error) {
|
||||
if (error instanceof FileAccessDeniedError) {
|
||||
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
|
||||
}
|
||||
logger.error(`[${requestId}] Error sending Slack message:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Logger } from '@sim/logger'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization'
|
||||
import type { ToolFileData } from '@/tools/types'
|
||||
|
||||
/**
|
||||
@@ -73,7 +74,8 @@ async function uploadFilesToSlack(
|
||||
files: any[],
|
||||
accessToken: string,
|
||||
requestId: string,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
ownerUserId: string
|
||||
): Promise<{ fileIds: string[]; files: ToolFileData[] }> {
|
||||
const userFiles = processFilesToUserFiles(files, requestId, logger)
|
||||
const uploadedFileIds: string[] = []
|
||||
@@ -82,6 +84,11 @@ async function uploadFilesToSlack(
|
||||
for (const userFile of userFiles) {
|
||||
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
|
||||
|
||||
const hasAccess = await verifyFileAccess(userFile.key, ownerUserId)
|
||||
if (!hasAccess) {
|
||||
throw new FileAccessDeniedError()
|
||||
}
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
|
||||
@@ -224,6 +231,7 @@ export interface SlackMessageParams {
|
||||
accessToken: string
|
||||
channel?: string
|
||||
userId?: string
|
||||
ownerUserId: string
|
||||
text: string
|
||||
threadTs?: string | null
|
||||
blocks?: unknown[] | null
|
||||
@@ -249,7 +257,7 @@ export async function sendSlackMessage(
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
const { accessToken, text, threadTs, blocks, files } = params
|
||||
const { accessToken, text, threadTs, blocks, files, ownerUserId } = params
|
||||
let { channel } = params
|
||||
|
||||
if (!channel && params.userId) {
|
||||
@@ -282,7 +290,8 @@ export async function sendSlackMessage(
|
||||
files,
|
||||
accessToken,
|
||||
requestId,
|
||||
logger
|
||||
logger,
|
||||
ownerUserId
|
||||
)
|
||||
|
||||
// No valid files uploaded - send text-only
|
||||
|
||||
@@ -22,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -33,8 +33,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
logger.info(`[${requestId}] Authenticated SMTP request via ${authResult.authType}`, {
|
||||
userId: authResult.userId,
|
||||
userId,
|
||||
})
|
||||
|
||||
const parsed = await parseRequest(smtpSendContract, request, {})
|
||||
@@ -120,25 +121,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const attachmentBuffers: { filename: string; content: Buffer; contentType: string }[] = []
|
||||
for (const file of attachments) {
|
||||
const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
try {
|
||||
logger.info(`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`)
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
attachmentBuffers.push({
|
||||
filename: file.name,
|
||||
content: buffer,
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
const accessResults = await Promise.all(
|
||||
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
|
||||
)
|
||||
const denied = accessResults.find((r) => r !== null)
|
||||
if (denied) return denied
|
||||
|
||||
const buffers = await Promise.all(
|
||||
attachments.map(async (file) => {
|
||||
try {
|
||||
logger.info(
|
||||
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
|
||||
)
|
||||
return await downloadFileFromStorage(file, requestId, logger)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
|
||||
throw new Error(
|
||||
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const attachmentBuffers = attachments.map((file, i) => ({
|
||||
filename: file.name,
|
||||
content: buffers[i],
|
||||
contentType: file.type || 'application/octet-stream',
|
||||
}))
|
||||
|
||||
logger.info(`[${requestId}] Processed ${attachmentBuffers.length} attachment(s)`)
|
||||
mailOptions.attachments = attachmentBuffers
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
downloadFileFromStorage,
|
||||
resolveInternalFileUrl,
|
||||
} from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import type { TranscriptSegment } from '@/tools/stt/types'
|
||||
|
||||
const logger = createLogger('SttProxyAPI')
|
||||
@@ -31,7 +32,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -79,6 +80,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const file = Array.isArray(body.audioFile) ? body.audioFile[0] : body.audioFile
|
||||
logger.info(`[${requestId}] Processing uploaded file: ${file.name}`)
|
||||
|
||||
const deniedAudio = await assertToolFileAccess(file.key, userId, requestId, logger)
|
||||
if (deniedAudio) return deniedAudio
|
||||
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
audioFileName = file.name
|
||||
// file.type may be missing if the file came from a block that doesn't preserve it
|
||||
@@ -97,6 +100,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
: body.audioFileReference
|
||||
logger.info(`[${requestId}] Processing referenced file: ${file.name}`)
|
||||
|
||||
const deniedRef = await assertToolFileAccess(file.key, userId, requestId, logger)
|
||||
if (deniedRef) return deniedRef
|
||||
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
audioFileName = file.name
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Unauthorized Supabase storage upload attempt: ${authResult.error}`
|
||||
)
|
||||
@@ -143,6 +144,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
uploadBody = buffer
|
||||
|
||||
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { convertMarkdownToHTML } from '@/tools/telegram/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
requireWorkflowId: false,
|
||||
})
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Telegram send attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -88,6 +89,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const userFile = userFiles[0]
|
||||
logger.info(`[${requestId}] Uploading document: ${userFile.name}`)
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
const filesOutput = [
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
downloadFileFromStorage,
|
||||
resolveInternalFileUrl,
|
||||
} from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 300 // 5 minutes for large multi-page PDF processing
|
||||
@@ -428,6 +429,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
)
|
||||
}
|
||||
|
||||
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
bytes = buffer.toString('base64')
|
||||
contentType = userFile.type || 'application/octet-stream'
|
||||
|
||||
@@ -8,6 +8,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('VideoProxyAPI')
|
||||
@@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -100,6 +101,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
let jobId: string | undefined
|
||||
let actualDuration: number | undefined
|
||||
|
||||
if (body.visualReference) {
|
||||
const denied = await assertToolFileAccess(
|
||||
body.visualReference.key,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
}
|
||||
|
||||
try {
|
||||
if (provider === 'runway') {
|
||||
const result = await generateWithRunway(
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
downloadFileFromStorage,
|
||||
resolveInternalFileUrl,
|
||||
} from '@/lib/uploads/utils/file-utils.server'
|
||||
import { assertToolFileAccess } from '@/app/api/files/authorization'
|
||||
import { convertUsageMetadata, extractTextContent } from '@/providers/google/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -27,7 +28,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized Vision analyze attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -87,6 +88,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
let base64 = userFile.base64
|
||||
let bufferLength = 0
|
||||
if (!base64) {
|
||||
const denied = await assertToolFileAccess(
|
||||
userFile.key,
|
||||
authResult.userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (denied) return denied
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
base64 = buffer.toString('base64')
|
||||
bufferLength = buffer.length
|
||||
|
||||
@@ -25,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized WordPress upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
|
||||
<Button
|
||||
key={tag.label}
|
||||
variant='subtle'
|
||||
className='max-w-[200px] px-2 py-1 text-caption'
|
||||
className='max-w-[280px] px-2 py-1 text-caption'
|
||||
onClick={tag.onRemove}
|
||||
>
|
||||
<span className='truncate'>{tag.label}</span>
|
||||
|
||||
+49
-56
@@ -178,64 +178,57 @@ const ResourceTabItem = memo(function ResourceTabItem({
|
||||
{showGapBefore && (
|
||||
<div className='-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 left-0 z-10 h-[16px] w-[2px] rounded-full bg-[var(--text-subtle)]' />
|
||||
)}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='subtle'
|
||||
draggable
|
||||
data-resource-tab-id={resource.id}
|
||||
onDragStart={(e) => onDragStart(e, idx)}
|
||||
onDragOver={(e) => onDragOver(e, idx)}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragEnd={onDragEnd}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
if (chatId) onRemove(e, resource)
|
||||
}
|
||||
<Button
|
||||
variant='subtle'
|
||||
draggable
|
||||
data-resource-tab-id={resource.id}
|
||||
onDragStart={(e) => onDragStart(e, idx)}
|
||||
onDragOver={(e) => onDragOver(e, idx)}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragEnd={onDragEnd}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault()
|
||||
if (chatId) onRemove(e, resource)
|
||||
}
|
||||
}}
|
||||
onClick={(e) => onTabClick(e, idx)}
|
||||
onMouseEnter={() => setHoveredTabId(resource.id)}
|
||||
onMouseLeave={() => setHoveredTabId(null)}
|
||||
className={cn(
|
||||
'group relative shrink-0 bg-transparent px-2 py-[3px] pr-[22px] text-caption transition-colors duration-150',
|
||||
isActive && 'bg-[var(--surface-4)]',
|
||||
isSelected && !isActive && 'bg-[var(--surface-3)]',
|
||||
isDragging && 'opacity-30'
|
||||
)}
|
||||
>
|
||||
{config.renderTabIcon(resource, 'mr-1.5 h-[14px] w-[14px]')}
|
||||
{displayName}
|
||||
{(isHovered || isActive) && chatId && (
|
||||
<span
|
||||
role='button'
|
||||
tabIndex={-1}
|
||||
onClick={(e) => onRemove(e, resource)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onRemove(e, resource)
|
||||
}}
|
||||
onClick={(e) => onTabClick(e, idx)}
|
||||
onMouseEnter={() => setHoveredTabId(resource.id)}
|
||||
onMouseLeave={() => setHoveredTabId(null)}
|
||||
className={cn(
|
||||
'group relative shrink-0 bg-transparent px-2 py-1 pr-[22px] text-caption transition-opacity duration-150',
|
||||
isActive && 'bg-[var(--surface-4)]',
|
||||
isSelected && !isActive && 'bg-[var(--surface-3)]',
|
||||
isDragging && 'opacity-30'
|
||||
)}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[4px] flex items-center justify-center rounded-sm p-[1px] hover-hover:bg-[var(--surface-5)]'
|
||||
aria-label={`Close ${displayName}`}
|
||||
>
|
||||
{config.renderTabIcon(resource, 'mr-1.5 h-[14px] w-[14px]')}
|
||||
{displayName}
|
||||
{(isHovered || isActive) && chatId && (
|
||||
<span
|
||||
role='button'
|
||||
tabIndex={-1}
|
||||
onClick={(e) => onRemove(e, resource)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') onRemove(e, resource)
|
||||
}}
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[4px] flex items-center justify-center rounded-sm p-[1px] hover-hover:bg-[var(--surface-5)]'
|
||||
aria-label={`Close ${displayName}`}
|
||||
>
|
||||
<svg
|
||||
className='size-[10px] text-[var(--text-icon)]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M18 6 6 18M6 6l12 12' />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
<p>{displayName}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<svg
|
||||
className='size-[10px] text-[var(--text-icon)]'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth='2.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
>
|
||||
<path d='M18 6 6 18M6 6l12 12' />
|
||||
</svg>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{showGapAfter && (
|
||||
<div className='-translate-y-1/2 pointer-events-none absolute top-1/2 right-0 z-10 h-[16px] w-[2px] translate-x-1/2 rounded-full bg-[var(--text-subtle)]' />
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,11 @@ import { hasActiveFilters } from '@/lib/logs/filters'
|
||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||
import { captureEvent } from '@/lib/posthog/client'
|
||||
import { workflowBorderColor } from '@/lib/workspaces/colors'
|
||||
import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import {
|
||||
formatDateShort,
|
||||
type LogStatus,
|
||||
STATUS_CONFIG,
|
||||
} from '@/app/workspace/[workspaceId]/logs/utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { useFolderMap } from '@/hooks/queries/folders'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
@@ -43,28 +47,6 @@ const TIME_RANGE_OPTIONS: ComboboxOption[] = [
|
||||
{ value: 'Custom range', label: 'Custom range' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Formats a date string (YYYY-MM-DD) for display.
|
||||
*/
|
||||
function formatDateShort(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
]
|
||||
return `${months[date.getMonth()]} ${date.getDate()}`
|
||||
}
|
||||
|
||||
type ViewMode = 'logs' | 'dashboard'
|
||||
|
||||
interface LogsToolbarProps {
|
||||
@@ -794,11 +776,13 @@ export const LogsToolbar = memo(function LogsToolbar({
|
||||
}
|
||||
size='sm'
|
||||
align='end'
|
||||
className='h-[32px] w-[120px] rounded-md'
|
||||
className='h-[32px] w-[160px] rounded-md'
|
||||
maxHeight={320}
|
||||
/>
|
||||
<DatePicker
|
||||
mode='range'
|
||||
showTrigger={false}
|
||||
showTime
|
||||
open={datePickerOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
DELETED_WORKFLOW_LABEL,
|
||||
extractRetryInput,
|
||||
formatDate,
|
||||
formatDateShort,
|
||||
getDisplayStatus,
|
||||
type LogStatus,
|
||||
parseDuration,
|
||||
@@ -205,25 +206,6 @@ function SpinningRefreshCw(props: React.SVGProps<SVGSVGElement>) {
|
||||
return <RefreshCw {...props} animate />
|
||||
}
|
||||
|
||||
function formatDateShort(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
]
|
||||
return `${months[date.getMonth()]} ${date.getDate()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs page component displaying workflow execution history.
|
||||
* Supports filtering, search, live updates, and detailed log inspection.
|
||||
@@ -866,7 +848,7 @@ export default function Logs() {
|
||||
tags.push({
|
||||
label:
|
||||
timeRange === 'Custom range' && startDate && endDate
|
||||
? `${startDate} – ${endDate}`
|
||||
? `${formatDateShort(startDate)} – ${formatDateShort(endDate)}`
|
||||
: timeRange,
|
||||
onRemove: () => {
|
||||
clearDateRange()
|
||||
@@ -1519,10 +1501,12 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
|
||||
}
|
||||
size='sm'
|
||||
className='h-[32px] w-full rounded-md'
|
||||
maxHeight={320}
|
||||
/>
|
||||
<DatePicker
|
||||
mode='range'
|
||||
showTrigger={false}
|
||||
showTime
|
||||
open={datePickerOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
|
||||
@@ -179,6 +179,31 @@ export function formatLatency(ms: number): string {
|
||||
return formatDuration(ms, { precision: 2 }) ?? '—'
|
||||
}
|
||||
|
||||
export function formatDateShort(dateStr: string): string {
|
||||
const hasTime = dateStr.includes('T')
|
||||
const [datePart, timePart] = dateStr.split('T')
|
||||
const [, month, day] = datePart.split('-').map(Number)
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
]
|
||||
const dateLabel = `${months[month - 1]} ${day}`
|
||||
if (hasTime && timePart) {
|
||||
return `${dateLabel} ${timePart.slice(0, 5)}`
|
||||
}
|
||||
return dateLabel
|
||||
}
|
||||
|
||||
export const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
} from '@/components/emcn/components/popover/popover'
|
||||
import { TimePicker } from '@/components/emcn/components/time-picker/time-picker'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
/**
|
||||
@@ -96,22 +97,26 @@ interface DatePickerSingleProps extends DatePickerBaseProps {
|
||||
onCancel?: never
|
||||
/** Not used in single mode */
|
||||
onClear?: never
|
||||
/** Not used in single mode */
|
||||
showTime?: never
|
||||
}
|
||||
|
||||
/** Props for range date mode */
|
||||
interface DatePickerRangeProps extends DatePickerBaseProps {
|
||||
/** Selection mode */
|
||||
mode: 'range'
|
||||
/** Start date for range mode (YYYY-MM-DD string or Date) */
|
||||
/** Start date for range mode (YYYY-MM-DD or YYYY-MM-DDTHH:mm string or Date) */
|
||||
startDate?: string | Date
|
||||
/** End date for range mode (YYYY-MM-DD string or Date) */
|
||||
/** End date for range mode (YYYY-MM-DD or YYYY-MM-DDTHH:mm string or Date) */
|
||||
endDate?: string | Date
|
||||
/** Callback when date range is applied */
|
||||
/** Callback when date range is applied — returns YYYY-MM-DD or YYYY-MM-DDTHH:mm depending on showTime */
|
||||
onRangeChange?: (startDate: string, endDate: string) => void
|
||||
/** Callback when range selection is cancelled */
|
||||
onCancel?: () => void
|
||||
/** Callback when range is cleared */
|
||||
onClear?: () => void
|
||||
/** Whether to show time inputs for precise range selection */
|
||||
showTime?: boolean
|
||||
/** Not used in range mode */
|
||||
value?: never
|
||||
/** Not used in range mode */
|
||||
@@ -503,6 +508,7 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
onRangeChange: _onRangeChange,
|
||||
onCancel: _onCancel,
|
||||
onClear: _onClear,
|
||||
showTime = false,
|
||||
...htmlProps
|
||||
} = rest as any
|
||||
|
||||
@@ -530,6 +536,8 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
const [rangeEnd, setRangeEnd] = React.useState<Date | null>(initialEnd)
|
||||
const [hoverDate, setHoverDate] = React.useState<Date | null>(null)
|
||||
const [selectingEnd, setSelectingEnd] = React.useState(false)
|
||||
const [startTime, setStartTime] = React.useState('00:00')
|
||||
const [endTime, setEndTime] = React.useState('23:59')
|
||||
|
||||
const [viewMonth, setViewMonth] = React.useState(() => {
|
||||
const d = selectedDate || initialStart || new Date()
|
||||
@@ -548,6 +556,12 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
setRangeStart(initialStart)
|
||||
setRangeEnd(initialEnd)
|
||||
setSelectingEnd(false)
|
||||
if (showTime) {
|
||||
const sd = isRangeMode ? props.startDate : undefined
|
||||
const ed = isRangeMode ? props.endDate : undefined
|
||||
setStartTime(typeof sd === 'string' && sd.includes('T') ? sd.slice(11, 16) : '00:00')
|
||||
setEndTime(typeof ed === 'string' && ed.includes('T') ? ed.slice(11, 16) : '23:59')
|
||||
}
|
||||
if (initialStart) {
|
||||
setViewMonth(initialStart.getMonth())
|
||||
setViewYear(initialStart.getFullYear())
|
||||
@@ -557,7 +571,7 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
setViewYear(now.getFullYear())
|
||||
}
|
||||
}
|
||||
}, [open, isRangeMode, initialStart, initialEnd])
|
||||
}, [open, isRangeMode, initialStart, initialEnd, showTime, props.startDate, props.endDate])
|
||||
|
||||
const singleValueKey = !isRangeMode && selectedDate ? selectedDate.getTime() : undefined
|
||||
const [prevSingleValueKey, setPrevSingleValueKey] = React.useState(singleValueKey)
|
||||
@@ -661,13 +675,32 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
if (isRangeMode && props.onRangeChange && rangeStart) {
|
||||
const start = rangeEnd && rangeEnd < rangeStart ? rangeEnd : rangeStart
|
||||
const end = rangeEnd && rangeEnd < rangeStart ? rangeStart : rangeEnd || rangeStart
|
||||
const startStr = formatDateAsString(start.getFullYear(), start.getMonth(), start.getDate())
|
||||
const endStr = formatDateAsString(end.getFullYear(), end.getMonth(), end.getDate())
|
||||
|
||||
let effectiveStartTime = startTime
|
||||
let effectiveEndTime = endTime
|
||||
if (showTime && startStr === endStr && startTime > endTime) {
|
||||
effectiveStartTime = endTime
|
||||
effectiveEndTime = startTime
|
||||
}
|
||||
|
||||
props.onRangeChange(
|
||||
formatDateAsString(start.getFullYear(), start.getMonth(), start.getDate()),
|
||||
formatDateAsString(end.getFullYear(), end.getMonth(), end.getDate())
|
||||
showTime ? `${startStr}T${effectiveStartTime}` : startStr,
|
||||
showTime ? `${endStr}T${effectiveEndTime}:59` : endStr
|
||||
)
|
||||
setOpen(false)
|
||||
}
|
||||
}, [isRangeMode, props.onRangeChange, rangeStart, rangeEnd, setOpen])
|
||||
}, [
|
||||
isRangeMode,
|
||||
props.onRangeChange,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
showTime,
|
||||
startTime,
|
||||
endTime,
|
||||
setOpen,
|
||||
])
|
||||
|
||||
/**
|
||||
* Cancels range selection.
|
||||
@@ -754,6 +787,21 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Time inputs */}
|
||||
{showTime && (
|
||||
<div className='flex border-[var(--border-1)] border-t'>
|
||||
<div className='flex flex-1 items-center justify-between gap-2 px-3 py-2'>
|
||||
<span className='shrink-0 text-[12px] text-[var(--text-muted)]'>Start</span>
|
||||
<TimePicker size='sm' value={startTime} onChange={setStartTime} />
|
||||
</div>
|
||||
<div className='w-[1px] bg-[var(--border-1)]' />
|
||||
<div className='flex flex-1 items-center justify-between gap-2 px-3 py-2'>
|
||||
<span className='shrink-0 text-[12px] text-[var(--text-muted)]'>End</span>
|
||||
<TimePicker size='sm' value={endTime} onChange={setEndTime} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className='flex items-center justify-between border-[var(--border-1)] border-t px-3 py-2'>
|
||||
<Button
|
||||
|
||||
@@ -243,7 +243,7 @@ export const sshReadFileContentBodySchema = requirePasswordOrPrivateKey(
|
||||
...connectionFields,
|
||||
path: z.string().min(1, 'Path is required'),
|
||||
encoding: z.string().default('utf-8'),
|
||||
maxSize: z.coerce.number().default(10),
|
||||
maxSize: z.coerce.number().min(0.01).max(50).default(10),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export function getStartDateFromTimeRange(timeRange: TimeRange, startDate?: stri
|
||||
if (timeRange === 'Custom range') {
|
||||
if (startDate) {
|
||||
const date = new Date(startDate)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
if (!startDate.includes('T')) date.setHours(0, 0, 0, 0)
|
||||
return date
|
||||
}
|
||||
return null
|
||||
@@ -111,7 +111,11 @@ export function getEndDateFromTimeRange(timeRange: TimeRange, endDate?: string):
|
||||
|
||||
if (endDate) {
|
||||
const date = new Date(endDate)
|
||||
date.setHours(23, 59, 59, 999)
|
||||
if (!endDate.includes('T')) {
|
||||
date.setHours(23, 59, 59, 999)
|
||||
} else {
|
||||
date.setMilliseconds(999)
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,6 @@ function validateAttioSignature(secret: string, signature: string, body: string)
|
||||
return false
|
||||
}
|
||||
const computedHash = hmacSha256Hex(body, secret)
|
||||
logger.debug('Attio signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
computedLength: computedHash.length,
|
||||
providedLength: signature.length,
|
||||
match: computedHash === signature,
|
||||
})
|
||||
return safeCompare(computedHash, signature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Attio signature:', error)
|
||||
@@ -65,10 +58,7 @@ export const attioHandler: WebhookProviderHandler = {
|
||||
const isValidSignature = validateAttioSignature(secret, signature, rawBody)
|
||||
|
||||
if (!isValidSignature) {
|
||||
logger.warn(`[${requestId}] Attio signature verification failed`, {
|
||||
signatureLength: signature.length,
|
||||
secretLength: secret.length,
|
||||
})
|
||||
logger.warn(`[${requestId}] Attio signature verification failed`)
|
||||
return new NextResponse('Unauthorized - Invalid Attio signature', {
|
||||
status: 401,
|
||||
})
|
||||
|
||||
@@ -37,14 +37,6 @@ function validateGitHubSignature(secret: string, signature: string, body: string
|
||||
return false
|
||||
}
|
||||
const computedHash = crypto.createHmac(algorithm, secret).update(body, 'utf8').digest('hex')
|
||||
logger.debug('GitHub signature comparison', {
|
||||
algorithm,
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${providedSignature.substring(0, 10)}...`,
|
||||
computedLength: computedHash.length,
|
||||
providedLength: providedSignature.length,
|
||||
match: computedHash === providedSignature,
|
||||
})
|
||||
return safeCompare(computedHash, providedSignature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating GitHub signature:', error)
|
||||
@@ -68,8 +60,6 @@ export const githubHandler: WebhookProviderHandler = {
|
||||
|
||||
if (!validateGitHubSignature(secret, signature, rawBody)) {
|
||||
logger.warn(`[${requestId}] GitHub signature verification failed`, {
|
||||
signatureLength: signature.length,
|
||||
secretLength: secret.length,
|
||||
usingSha256: !!request.headers.get('X-Hub-Signature-256'),
|
||||
})
|
||||
return new NextResponse('Unauthorized - Invalid GitHub signature', { status: 401 })
|
||||
|
||||
@@ -59,10 +59,7 @@ export const intercomHandler: WebhookProviderHandler = {
|
||||
}
|
||||
|
||||
if (!validateIntercomSignature(secret, signature, rawBody)) {
|
||||
logger.warn(`[${requestId}] Intercom signature verification failed`, {
|
||||
signatureLength: signature.length,
|
||||
secretLength: secret.length,
|
||||
})
|
||||
logger.warn(`[${requestId}] Intercom signature verification failed`)
|
||||
return new NextResponse('Unauthorized - Invalid Intercom signature', { status: 401 })
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ async function handleWhatsAppVerification(
|
||||
continue
|
||||
}
|
||||
|
||||
if (token === verificationToken) {
|
||||
if (safeCompare(token, verificationToken as string)) {
|
||||
logger.info(`[${requestId}] WhatsApp verification successful for webhook ${wh.id}`)
|
||||
return new NextResponse(challenge, {
|
||||
status: 200,
|
||||
|
||||
@@ -1390,7 +1390,7 @@ export class PauseResumeManager {
|
||||
await tx
|
||||
.update(pausedExecutions)
|
||||
.set({
|
||||
pausePoints: sql`jsonb_set(jsonb_set(pause_points, ARRAY[${contextId}, 'resumeStatus'], '"resumed"'::jsonb), ARRAY[${contextId}, 'resumedAt'], '"${sql.raw(now.toISOString())}"'::jsonb)`,
|
||||
pausePoints: sql`jsonb_set(jsonb_set(pause_points, ARRAY[${contextId}, 'resumeStatus'], '"resumed"'::jsonb), ARRAY[${contextId}, 'resumedAt'], ${JSON.stringify(now.toISOString())}::jsonb)`,
|
||||
resumedCount: sql`resumed_count + 1`,
|
||||
status: sql`CASE WHEN status = 'cancelling' THEN 'cancelling' WHEN resumed_count + 1 >= total_pause_count THEN 'fully_resumed' ELSE 'partially_resumed' END`,
|
||||
updatedAt: now,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Logger } from '@sim/logger'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { GraphApiErrorResponse, GraphDriveItem } from '@/tools/microsoft_teams/types'
|
||||
|
||||
@@ -45,8 +46,9 @@ export async function uploadFilesForTeamsMessage(params: {
|
||||
accessToken: string
|
||||
requestId: string
|
||||
logger: Logger
|
||||
userId: string
|
||||
}): Promise<TeamsFileUploadResult> {
|
||||
const { rawFiles, accessToken, requestId, logger: log } = params
|
||||
const { rawFiles, accessToken, requestId, logger: log, userId } = params
|
||||
const attachments: TeamsAttachmentRef[] = []
|
||||
const filesOutput: TeamsFileOutput[] = []
|
||||
|
||||
@@ -72,6 +74,11 @@ export async function uploadFilesForTeamsMessage(params: {
|
||||
|
||||
log.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
|
||||
|
||||
const hasAccess = await verifyFileAccess(file.key, userId)
|
||||
if (!hasAccess) {
|
||||
throw new FileAccessDeniedError()
|
||||
}
|
||||
|
||||
// Download file from storage
|
||||
const buffer = await downloadFileFromStorage(file, requestId, log)
|
||||
filesOutput.push({
|
||||
|
||||
@@ -368,7 +368,7 @@ export async function resolveMentionsForChat(
|
||||
})
|
||||
}
|
||||
resolvedTags.add(mention.fullTag)
|
||||
updatedContent = updatedContent.replace(
|
||||
updatedContent = updatedContent.replaceAll(
|
||||
mention.fullTag,
|
||||
`<at id="${mention.mentionId}">${mention.name}</at>`
|
||||
)
|
||||
@@ -435,7 +435,7 @@ export async function resolveMentionsForChannel(
|
||||
})
|
||||
}
|
||||
resolvedTags.add(mention.fullTag)
|
||||
updatedContent = updatedContent.replace(
|
||||
updatedContent = updatedContent.replaceAll(
|
||||
mention.fullTag,
|
||||
`<at id="${mention.mentionId}">${mention.name}</at>`
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user