I am using TypeScript and Next.js 14 and my s3 bucket is working perfectly in localhost but not in production. I did create a separate bucket with policy and IAM user for production as well. All the keys match up, but I am receiving an error of CORS and a 403 forbidden error as well in production.
Inside the payload for the errors I am receiving an UNSIGNED-PAYLOAD for the X-Amz-Content-Sha256.
The error is happening on the handleAudioUploadtoS3 function
Here is my client side code:
import Image from "next/image";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Montserrat } from "next/font/google";
import { cn } from "@/lib/utils";
import { useState, useRef } from "react";
import { useRouter } from "next/navigation";
import { useProModal } from "@/hooks/use-pro-modal";
import { Download } from "lucide-react";
import { ImSpinner3 } from "react-icons/im";
import { getSignedURL } from "@/app/_actions/actions";
import { MAX_CHARACTERS } from "@/constants";
import { toast } from "react-hot-toast";
const montserrat = Montserrat({
weight: "600",
subsets: ["latin"],
});
export default function YourVoicesPage({
params,
}: {
params: { id: string; image: string; name: string; flag: string };
}) {
const sourceElem = useRef(null);
const router = useRouter();
const proModal = useProModal();
const [voice, setVoice] = useState("");
const [response, setResponse] = useState<Blob | "">("");
const [converting, setConverting] = useState(false);
const [characterCount, setCharacterCount] = useState(0);
// console.log("params", params);
// using the params to get the correct name, image and flag
const nameString = params.id[1].split("%26")[1];
const name = nameString.split("%3D")[1];
const imageString = params.id[1].split("%26")[0];
const correctImagePath = imageString.startsWith("/")
? imageString
: `/${imageString}`;
console.log("image", correctImagePath);
const flagString = params.id[2];
const correctFlagPath = flagString.startsWith("/")
? flagString
: `/${flagString}`;
console.log("flag", correctFlagPath);
// using the params to get the correct voiceID
const voiceIDString = params.id[0];
const voiceID = voiceIDString.replace("%26image%3D", "");
const handleTextChange = (e: any) => {
const newText = e.target.value;
setCharacterCount(newText.length);
setVoice(newText); // Update the state with the new text
};
const isGenerateDisabled = characterCount > MAX_CHARACTERS;
// Upload and save Audio to S3 bucket & database
const handleAudioUploadtoS3 = async (audioBlob: Blob) => {
console.log("audioBlob", audioBlob);
const signedURLResult = await getSignedURL({ name, correctImagePath, text:voice });
console.log("signedURL", signedURLResult);
if (!signedURLResult.success) {
console.log("error", signedURLResult.error);
return;
}
const url = signedURLResult.success.url;
console.log("url", url);
const response = await fetch(url, {
method: "PUT",
body: audioBlob,
headers: {
"Content-Type": "audio/mpeg",
},
});
if(!response.ok) {
console.log("error", response);
return;
}
if(response.ok) {
toast.success("Audio file saved to your dashboard");
}
console.log("response", response);
};
// Create the audio from the user's input text
const handleVoiceInput = async () => {
try {
setConverting(true);
const response = await fetch("/api/voice-creation", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
voiceID: voiceID,
text: voice,
characterCount: characterCount,
}),
});
if (!response.ok) {
if (response.status === 403) {
setConverting(false);
proModal.onOpen();
} else {
console.log("Unexpected response status:", response.status);
}
return; // Stop further processing
}
const blobResponse = await response.blob();
setResponse(blobResponse);
} catch (error: any) {
console.log("error", error);
if (error?.response?.status === 403) {
proModal.onOpen();
}
} finally {
setConverting(false);
router.refresh();
}
};
// console.log("voiceID", voiceID);
return (
<div className="flex w-full h-screen bg-slate-100">
<div className="max-w-7xl mx-auto flex flex-col">
<h1
className={cn(
"mt-10 font-bold text-center text-4xl mb-10",
montserrat.className
)}
>
Your Voices
</h1>
<div className="flex flex-col mb-8">
<Image src={correctImagePath} alt="name" width={250} height={250} />
<div className="flex flex-row gap-2 mx-auto mb-6 items-center">
<h3 className="font-semibold text-2xl">{name}</h3>
<Image src={correctFlagPath} alt="flag" width={50} height={50} />
</div>
<div className="flex flex-col gap-4">
<Textarea
className=""
value={voice}
onChange={handleTextChange}
placeholder="Type your text here..."
/>
<p className={voice.length > 200 ? "text-red-500" : ""}>
{/* Character Count: {voice.length}/200 */}
Character Count: {characterCount}
</p>
<Button disabled={isGenerateDisabled} onClick={handleVoiceInput}>
Generate
{converting && (
<span className="animate-spin text-lg ml-3">
<ImSpinner3 />
</span>
)}
</Button>
</div>
</div>
{response && (
<>
<audio
src={response ? URL.createObjectURL(response) : ""}
controls
/>
{/* <source ref={sourceElem} src={response} type="audio/mpeg" /> */}
<div className="flex flex-row gap-6 items-center mt-10">
<a
className="flex items-center gap-4"
href={response ? URL.createObjectURL(response) : ""}
download
>
{response ? (
<div className="flex gap-6 items-center">
<Button className="flex gap-5">
<Download size={16} className="text-white" />{" "}
<p className="text-white">Download Audio File</p>
</Button>
</div>
) : (
""
)}
</a>
<Button variant={'outline'}
onClick={() => handleAudioUploadtoS3(response)}
>
Save to Your Dashboard
</Button>
</div>
</>
)}
</div>
</div>
);
}
Here is my server code:
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/utils/authOptions";
import {
S3Client,
PutObjectCommand,
DeleteObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import prismadb from "@/lib/prismadb";
const s3 = new S3Client({
region: process.env.AWS_BUCKET_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function getSignedURL({
name,
correctImagePath,
text
}: {
name: string;
correctImagePath: string;
text: string
}) {
// Get the user's session
const session = await getServerSession(authOptions);
if (!session) return { error: "Not authenticated" };
console.log("testing out", name, correctImagePath);
const timestamp = new Date().toISOString();
const key = `users/${session.user.id}/audio/${timestamp}_audioFile.mp3`;
const putObjctCommand = new PutObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME!,
Key: key,
Metadata: {
userId: session.user.id,
name: name,
image: correctImagePath,
},
});
const signedURL = await getSignedUrl(s3, putObjctCommand, {
expiresIn: 60,
});
const newAudio = await prismadb.audioFile.create({
data: {
userId: session.user.id,
url: signedURL.split("?")[0],
fileName: `${timestamp}_audioFile.mp3`,
image: correctImagePath,
aiName: name,
text: text
},
});
console.log("newAudio", newAudio)
return { success: { url: signedURL, audio: newAudio } };
}
export async function deleteAudio({ audioId }: { audioId: string }) {
// Get the user's session
console.log("testing out", audioId);
const session = await getServerSession(authOptions);
if (!session) return { error: "Not authenticated" };
const audio = await prismadb.audioFile.findUnique({
where: { id: audioId },
});
console.log("audio", audio);
if (!audio) return { error: "Audio file not found" };
if (audio.userId !== session.user.id) {
return { error: "You do not have permission to delete this audio file" };
}
// delete from s3
const deleteObjctCommand = new DeleteObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME!,
Key: `users/${session.user.id}/audio/${audio.fileName}`,
});
await s3.send(deleteObjctCommand);
await prismadb.audioFile.delete({
where: { id: audioId },
});
return { success: "Audio file deleted" };
}
If you think you need more information to help me solve this that would be appreciated.