1- import { NextRequest , NextResponse } from "next/server" ;
2- import { z } from "zod" ;
3- import { AwsClient } from "aws4fetch" ;
4- import { nanoid } from "nanoid" ;
5- import { env } from "@/env" ;
6- import { baseRateLimit } from "@/lib/rate-limit" ;
7- import { isTranscriptionConfigured } from "@/lib/transcription-utils" ;
8-
9- const uploadRequestSchema = z . object ( {
10- fileExtension : z . enum ( [ "wav" , "mp3" , "m4a" , "flac" ] , {
11- errorMap : ( ) => ( {
12- message : "File extension must be wav, mp3, m4a, or flac" ,
13- } ) ,
14- } ) ,
15- } ) ;
16-
17- const apiResponseSchema = z . object ( {
18- uploadUrl : z . string ( ) . url ( ) ,
19- fileName : z . string ( ) . min ( 1 ) ,
20- } ) ;
21-
22- export async function POST ( request : NextRequest ) {
23- try {
24- // Rate limiting
25- const ip = request . headers . get ( "x-forwarded-for" ) ?? "anonymous" ;
26- const { success } = await baseRateLimit . limit ( ip ) ;
27-
28- if ( ! success ) {
29- return NextResponse . json ( { error : "Too many requests" } , { status : 429 } ) ;
30- }
31-
32- // Check transcription configuration
33- const transcriptionCheck = isTranscriptionConfigured ( ) ;
34- if ( ! transcriptionCheck . configured ) {
35- console . error (
36- "Missing environment variables:" ,
37- JSON . stringify ( transcriptionCheck . missingVars )
38- ) ;
39-
40- return NextResponse . json (
41- {
42- error : "Transcription not configured" ,
43- message : `Auto-captions require environment variables: ${ transcriptionCheck . missingVars . join ( ", " ) } . Check README for setup instructions.` ,
44- } ,
45- { status : 503 }
46- ) ;
47- }
48-
49- // Parse and validate request body
50- const rawBody = await request . json ( ) . catch ( ( ) => null ) ;
51- if ( ! rawBody ) {
52- return NextResponse . json (
53- { error : "Invalid JSON in request body" } ,
54- { status : 400 }
55- ) ;
56- }
57-
58- const validationResult = uploadRequestSchema . safeParse ( rawBody ) ;
59- if ( ! validationResult . success ) {
60- return NextResponse . json (
61- {
62- error : "Invalid request parameters" ,
63- details : validationResult . error . flatten ( ) . fieldErrors ,
64- } ,
65- { status : 400 }
66- ) ;
67- }
68-
69- const { fileExtension } = validationResult . data ;
70-
71- // Initialize R2 client
72- const client = new AwsClient ( {
73- accessKeyId : env . R2_ACCESS_KEY_ID ,
74- secretAccessKey : env . R2_SECRET_ACCESS_KEY ,
75- } ) ;
76-
77- // Generate unique filename with timestamp
78- const timestamp = Date . now ( ) ;
79- const fileName = `audio/${ timestamp } -${ nanoid ( ) } .${ fileExtension } ` ;
80-
81- // Create presigned URL
82- const url = new URL (
83- `https://${ env . R2_BUCKET_NAME } .${ env . CLOUDFLARE_ACCOUNT_ID } .r2.cloudflarestorage.com/${ fileName } `
84- ) ;
85-
86- url . searchParams . set ( "X-Amz-Expires" , "3600" ) ; // 1 hour expiry
87-
88- const signed = await client . sign ( new Request ( url , { method : "PUT" } ) , {
89- aws : { signQuery : true } ,
90- } ) ;
91-
92- if ( ! signed . url ) {
93- throw new Error ( "Failed to generate presigned URL" ) ;
94- }
95-
96- // Prepare and validate response
97- const responseData = {
98- uploadUrl : signed . url ,
99- fileName,
100- } ;
101-
102- const responseValidation = apiResponseSchema . safeParse ( responseData ) ;
103- if ( ! responseValidation . success ) {
104- console . error (
105- "Invalid API response structure:" ,
106- responseValidation . error
107- ) ;
108- return NextResponse . json (
109- { error : "Internal response formatting error" } ,
110- { status : 500 }
111- ) ;
112- }
113-
114- return NextResponse . json ( responseValidation . data ) ;
115- } catch ( error ) {
116- console . error ( "Error generating upload URL:" , error ) ;
117- return NextResponse . json (
118- {
119- error : "Failed to generate upload URL" ,
120- message :
121- error instanceof Error
122- ? error . message
123- : "An unexpected error occurred" ,
124- } ,
125- { status : 500 }
126- ) ;
127- }
128- }
1+ import { NextRequest , NextResponse } from "next/server" ;
2+ import { z } from "zod" ;
3+ import { AwsClient } from "aws4fetch" ;
4+ import { nanoid } from "nanoid" ;
5+ import { env } from "@/env" ;
6+ import { baseRateLimit } from "@/lib/rate-limit" ;
7+ import { isTranscriptionConfigured } from "@/lib/transcription-utils" ;
8+
9+ const uploadRequestSchema = z . object ( {
10+ fileExtension : z . enum ( [ "wav" , "mp3" , "m4a" , "flac" ] , {
11+ errorMap : ( ) => ( {
12+ message : "File extension must be wav, mp3, m4a, or flac" ,
13+ } ) ,
14+ } ) ,
15+ } ) ;
16+
17+ const apiResponseSchema = z . object ( {
18+ uploadUrl : z . string ( ) . url ( ) ,
19+ fileName : z . string ( ) . min ( 1 ) ,
20+ } ) ;
21+
22+ /**
23+ * Generates a presigned upload URL and a unique filename for uploading an audio file to Cloudflare R2.
24+ *
25+ * Accepts a JSON request body with a `fileExtension` (one of "wav", "mp3", "m4a", "flac"). Applies client rate limiting and verifies required transcription environment configuration before producing a signed PUT URL valid for 1 hour.
26+ *
27+ * @param request - Incoming Next.js request whose JSON body must include `fileExtension`
28+ * @returns On success, an object with `uploadUrl` (the presigned PUT URL) and `fileName` (the generated object path). On failure, a JSON error object with an `error` field and optional `message`/`details`; responses use appropriate HTTP status codes (400, 429, 503, 500).
29+ */
30+ export async function POST ( request : NextRequest ) {
31+ try {
32+ // Rate limiting
33+ const ip = request . headers . get ( "x-forwarded-for" ) ?? "anonymous" ;
34+ const { success } = await baseRateLimit . limit ( ip ) ;
35+
36+ if ( ! success ) {
37+ return NextResponse . json ( { error : "Too many requests" } , { status : 429 } ) ;
38+ }
39+
40+ // Check transcription configuration
41+ const transcriptionCheck = isTranscriptionConfigured ( ) ;
42+ if ( ! transcriptionCheck . configured ) {
43+ console . error (
44+ "Missing environment variables:" ,
45+ JSON . stringify ( transcriptionCheck . missingVars )
46+ ) ;
47+
48+ return NextResponse . json (
49+ {
50+ error : "Transcription not configured" ,
51+ message : `Auto-captions require environment variables: ${ transcriptionCheck . missingVars . join ( ", " ) } . Check README for setup instructions.` ,
52+ } ,
53+ { status : 503 }
54+ ) ;
55+ }
56+
57+ // Parse and validate request body
58+ const rawBody = await request . json ( ) . catch ( ( ) => null ) ;
59+ if ( ! rawBody ) {
60+ return NextResponse . json (
61+ { error : "Invalid JSON in request body" } ,
62+ { status : 400 }
63+ ) ;
64+ }
65+
66+ const validationResult = uploadRequestSchema . safeParse ( rawBody ) ;
67+ if ( ! validationResult . success ) {
68+ return NextResponse . json (
69+ {
70+ error : "Invalid request parameters" ,
71+ details : validationResult . error . flatten ( ) . fieldErrors ,
72+ } ,
73+ { status : 400 }
74+ ) ;
75+ }
76+
77+ const { fileExtension } = validationResult . data ;
78+
79+ // Initialize R2 client
80+ const client = new AwsClient ( {
81+ accessKeyId : env . R2_ACCESS_KEY_ID ,
82+ secretAccessKey : env . R2_SECRET_ACCESS_KEY ,
83+ } ) ;
84+
85+ // Generate unique filename with timestamp
86+ const timestamp = Date . now ( ) ;
87+ const fileName = `audio/${ timestamp } -${ nanoid ( ) } .${ fileExtension } ` ;
88+
89+ // Create presigned URL
90+ const url = new URL (
91+ `https://${ env . R2_BUCKET_NAME } .${ env . CLOUDFLARE_ACCOUNT_ID } .r2.cloudflarestorage.com/${ fileName } `
92+ ) ;
93+
94+ url . searchParams . set ( "X-Amz-Expires" , "3600" ) ; // 1 hour expiry
95+
96+ const signed = await client . sign ( new Request ( url , { method : "PUT" } ) , {
97+ aws : { signQuery : true } ,
98+ } ) ;
99+
100+ if ( ! signed . url ) {
101+ throw new Error ( "Failed to generate presigned URL" ) ;
102+ }
103+
104+ // Prepare and validate response
105+ const responseData = {
106+ uploadUrl : signed . url ,
107+ fileName,
108+ } ;
109+
110+ const responseValidation = apiResponseSchema . safeParse ( responseData ) ;
111+ if ( ! responseValidation . success ) {
112+ console . error (
113+ "Invalid API response structure:" ,
114+ responseValidation . error
115+ ) ;
116+ return NextResponse . json (
117+ { error : "Internal response formatting error" } ,
118+ { status : 500 }
119+ ) ;
120+ }
121+
122+ return NextResponse . json ( responseValidation . data ) ;
123+ } catch ( error ) {
124+ console . error ( "Error generating upload URL:" , error ) ;
125+ return NextResponse . json (
126+ {
127+ error : "Failed to generate upload URL" ,
128+ message :
129+ error instanceof Error
130+ ? error . message
131+ : "An unexpected error occurred" ,
132+ } ,
133+ { status : 500 }
134+ ) ;
135+ }
136+ }
0 commit comments