Files
zoeissleeping 8ccaa824dd feat: file upload retry, secure file tokens, UI polish
- Add HMAC-based file token auth for secure AI model file access
- Add file upload retry with exponential backoff (max 3 retries)
- File endpoint now requires session auth or signed token
- Support assistant role messages in chat input
- Optimistic UI for attachments on message send
- Verify topic ownership before allowing messages
- Switch web scraping to Firecrawl API
- Agent profile page layout fixes (proper flex overflow)
- Add quick switcher (Ctrl+K) to sidenav
- Clean up longcat.ts and stale comments
2026-06-06 00:16:39 -05:00

88 lines
2.4 KiB
TypeScript

import * as z from 'zod';
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export default defineEventHandler(async (event) => {
await protectRoute(event);
const result = await readValidatedBody(event, (body) =>
z
.object({
file: z.object({
name: z.string(),
mimeType: z.string(),
}),
})
.safeParse(body),
);
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.issues[0]!.message,
});
}
const config = useRuntimeConfig();
const { file } = result.data;
const s3 = new S3Client({
region: config.s3.region || 'us-east-1',
endpoint: config.s3.endpoint!,
credentials: {
accessKeyId: config.s3.accessKeyId!,
secretAccessKey: config.s3.secret!,
},
forcePathStyle: true,
requestChecksumCalculation: 'WHEN_REQUIRED',
responseChecksumValidation: 'WHEN_REQUIRED',
});
const fileParts = file.name.split('.');
if (fileParts.length < 2) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid file name',
data: {
code: 'INVALID_FILE_NAME',
ok: false,
}
});
}
const fileExt = fileParts.pop()!;
const fileName = fileParts.join('.').replace(/[^a-zA-Z0-9.]/g, '_');
const key = `veridian__uploads/${Date.now()}-${fileName}-${event.context.user!.id}.${fileExt}`;
const command = new PutObjectCommand({
ACL: 'public-read',
Bucket: config.s3.bucket!,
Key: key,
ContentType: file.mimeType,
});
try {
const url = await getSignedUrl(s3, command, {
expiresIn: 600,
signableHeaders: new Set(['content-type']),
});
return {
url,
assetUrl: `/api/files/${key}`,
};
} catch (error) {
console.error('Failed to generate presigned URL:', error);
throw createError({
statusCode: 500,
statusMessage: 'Failed to generate presigned URL',
data: {
code: 'INTERNAL_ERROR',
ok: false,
}
});
}
})