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

71 lines
2.2 KiB
TypeScript

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { auth } from "~~/lib/auth";
import { verifyFileToken } from "~~/server/utils/file-token";
export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key');
if (!key) {
throw createError({ statusCode: 400, statusMessage: 'Missing file key' });
}
const query = getQuery(event);
const exp = query.exp ? Number(query.exp) : undefined;
const sig = query.sig as string | undefined;
let authorized = false;
// Path 1: HMAC token (for AI model access)
if (exp && sig && process.env.BETTER_AUTH_SECRET) {
authorized = verifyFileToken(key, exp, sig, process.env.BETTER_AUTH_SECRET);
}
// Path 2: Session auth (for client-side access)
if (!authorized) {
try {
const sessionData = await auth.api.getSession(event);
if (sessionData) {
event.context.user = sessionData.user;
authorized = true;
}
} catch {
// No valid session
}
}
if (!authorized) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
const config = useRuntimeConfig();
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,
});
try {
const response = await s3.send(new GetObjectCommand({
Bucket: config.s3.bucket!,
Key: key,
}));
setHeaders(event, {
'Content-Type': response.ContentType || 'application/octet-stream',
'Content-Length': response.ContentLength?.toString() || '',
'Cache-Control': 'public, max-age=3600',
});
return response.Body;
} catch (error: any) {
if (error.name === 'NoSuchKey') {
throw createError({ statusCode: 404, statusMessage: 'File not found' });
}
throw createError({ statusCode: 500, statusMessage: 'Error fetching file' });
}
});