129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { AwsClient } from "aws4fetch";
|
|
import { nanoid } from "nanoid";
|
|
import { env } from "@/env";
|
|
import { baseRateLimit } from "@/lib/rate-limit";
|
|
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
|
|
|
|
const uploadRequestSchema = z.object({
|
|
fileExtension: z.enum(["wav", "mp3", "m4a", "flac"], {
|
|
errorMap: () => ({
|
|
message: "File extension must be wav, mp3, m4a, or flac",
|
|
}),
|
|
}),
|
|
});
|
|
|
|
const apiResponseSchema = z.object({
|
|
uploadUrl: z.string().url(),
|
|
fileName: z.string().min(1),
|
|
});
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// Rate limiting
|
|
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
const { success } = await baseRateLimit.limit(ip);
|
|
|
|
if (!success) {
|
|
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
}
|
|
|
|
// Check transcription configuration
|
|
const transcriptionCheck = isTranscriptionConfigured();
|
|
if (!transcriptionCheck.configured) {
|
|
console.error(
|
|
"Missing environment variables:",
|
|
JSON.stringify(transcriptionCheck.missingVars)
|
|
);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: "Transcription not configured",
|
|
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
|
|
},
|
|
{ status: 503 }
|
|
);
|
|
}
|
|
|
|
// Parse and validate request body
|
|
const rawBody = await request.json().catch(() => null);
|
|
if (!rawBody) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid JSON in request body" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const validationResult = uploadRequestSchema.safeParse(rawBody);
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Invalid request parameters",
|
|
details: validationResult.error.flatten().fieldErrors,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const { fileExtension } = validationResult.data;
|
|
|
|
// Initialize R2 client
|
|
const client = new AwsClient({
|
|
accessKeyId: env.R2_ACCESS_KEY_ID,
|
|
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
|
|
});
|
|
|
|
// Generate unique filename with timestamp
|
|
const timestamp = Date.now();
|
|
const fileName = `audio/${timestamp}-${nanoid()}.${fileExtension}`;
|
|
|
|
// Create presigned URL
|
|
const url = new URL(
|
|
`https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`
|
|
);
|
|
|
|
url.searchParams.set("X-Amz-Expires", "3600"); // 1 hour expiry
|
|
|
|
const signed = await client.sign(new Request(url, { method: "PUT" }), {
|
|
aws: { signQuery: true },
|
|
});
|
|
|
|
if (!signed.url) {
|
|
throw new Error("Failed to generate presigned URL");
|
|
}
|
|
|
|
// Prepare and validate response
|
|
const responseData = {
|
|
uploadUrl: signed.url,
|
|
fileName,
|
|
};
|
|
|
|
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
if (!responseValidation.success) {
|
|
console.error(
|
|
"Invalid API response structure:",
|
|
responseValidation.error
|
|
);
|
|
return NextResponse.json(
|
|
{ error: "Internal response formatting error" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(responseValidation.data);
|
|
} catch (error) {
|
|
console.error("Error generating upload URL:", error);
|
|
return NextResponse.json(
|
|
{
|
|
error: "Failed to generate upload URL",
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: "An unexpected error occurred",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|