-
Couldn't load subscription status.
- Fork 4k
fix: prevent hydration mismatch in ThemeToggle #611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,4 +31,4 @@ | |
| "breakpoints": true | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,128 +1,128 @@ | ||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| 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 }, | ||
| }); | ||
|
Comment on lines
+82
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify R2 endpoint style to prevent signature mismatch Ensure the host/path style matches Cloudflare R2 S3 signing rules. Many setups use Run to compare usage patterns in-repo: If we’re mixing styles across endpoints, standardize to one that matches our bucket configuration (virtual-hosted vs path-style) and test a PUT with the presigned URL. 🏁 Script executed: #!/bin/bash
rg -n -C2 'r2\.cloudflarestorage\.com' | sed -n '1,200p'Length of output: 891 Switch to path-style R2 endpoint for presigned URLs // from:
new URL(
`https://${env.R2_BUCKET_NAME}.${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`
);
// to:
new URL(
`https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${env.R2_BUCKET_NAME}/${fileName}`
);This matches your Python usage and Cloudflare R2’s signing rules to avoid 403s. 🤖 Prompt for AI Agents |
||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong configuration guard: this endpoint checks transcription config instead of storage config
This route only needs R2/Cloudflare vars. Using
isTranscriptionConfigured()can 503 the endpoint whenMODAL_TRANSCRIPTION_URLis missing, even though uploads don’t require it.Apply:
📝 Committable suggestion
🤖 Prompt for AI Agents