Accept CDN URLs for question attachments

Attachments uploaded via question-upload get a CDN URL
(files.second-brain.ru via S3_CDN_URL), but the validator only accepted
the direct S3 endpoint prefix — so every attachment was silently dropped
(message text saved, file lost). Add isAllowedPublicUrl (CDN + direct S3)
in lib/s3 and use it in both question routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 15:11:36 +05:00
parent 2436f77de5
commit 9fbb7aea6c
3 changed files with 19 additions and 21 deletions
+2 -11
View File
@@ -3,6 +3,7 @@ import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { sendQuestionFollowUpEmail, sendQuestionReplyEmail } from "@/lib/email";
import { isAllowedPublicUrl } from "@/lib/s3";
interface FileAttachment {
name: string;
@@ -10,13 +11,6 @@ interface FileAttachment {
size: number;
}
function buildS3Prefix(): string {
const endpoint = process.env.S3_ENDPOINT ?? "";
const bucket = process.env.S3_BUCKET ?? "";
// e.g. https://fsn1.your-objectstorage.com/lms-uploads/
return `${endpoint}/${bucket}/`;
}
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
@@ -52,14 +46,11 @@ export async function POST(
return NextResponse.json({ error: "text is required" }, { status: 400 });
}
const s3Prefix = buildS3Prefix();
const safeFiles = files
?.filter(
(f) =>
typeof f.name === "string" &&
typeof f.url === "string" &&
f.url.startsWith("https://") &&
f.url.startsWith(s3Prefix) &&
isAllowedPublicUrl(f.url) &&
typeof f.size === "number"
)
.map((f) => ({ name: f.name.slice(0, 255), url: f.url, size: Math.max(0, f.size) }));
+2 -10
View File
@@ -3,6 +3,7 @@ import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { sendQuestionCreatedEmail } from "@/lib/email";
import { isAllowedPublicUrl } from "@/lib/s3";
interface FileAttachment {
name: string;
@@ -10,12 +11,6 @@ interface FileAttachment {
size: number;
}
function buildS3Prefix(): string {
const endpoint = process.env.S3_ENDPOINT ?? "";
const bucket = process.env.S3_BUCKET ?? "";
return `${endpoint}/${bucket}/`;
}
export async function GET() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
@@ -82,14 +77,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "title and text are required" }, { status: 400 });
}
const s3Prefix = buildS3Prefix();
const safeFiles = files
?.filter(
(f) =>
typeof f.name === "string" &&
typeof f.url === "string" &&
f.url.startsWith("https://") &&
f.url.startsWith(s3Prefix) &&
isAllowedPublicUrl(f.url) &&
typeof f.size === "number"
)
.map((f) => ({ name: f.name.slice(0, 255), url: f.url, size: Math.max(0, f.size) }));