Skip to content

Commit

Permalink
Fix image upload on articles (#1126)
Browse files Browse the repository at this point in the history
* Fix image upload on articles
* Adds safe actions for actions
* Uses actions instead of trpc
  • Loading branch information
NiallJoeMaher authored Oct 15, 2024
1 parent 9803fd1 commit a47e8d2
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 29 deletions.
63 changes: 36 additions & 27 deletions app/(app)/create/[[...paramsArr]]/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ import copy from "copy-to-clipboard";
import { PostStatus, getPostStatus, isValidScheduleTime } from "@/utils/post";
import { ImageUp, LoaderCircle } from "lucide-react";
import { uploadFile } from "@/utils/s3helpers";
import { type Session } from "next-auth";
import { getUploadUrl } from "@/app/actions/getUploadUrl";

const Create = ({ session }: { session: Session }) => {
const Create = () => {
const params = useParams();
const router = useRouter();

Expand Down Expand Up @@ -71,29 +71,40 @@ const Create = ({ session }: { session: Session }) => {
const file = e.target.files[0];
const { size, type } = file;

await getUploadUrl(
{ size, type, config: { kind: "uploads", userId: session.user?.id } },
{
onError(error) {
setUploadStatus("error");
if (error) return toast.error(error.message);
return toast.error(
"Something went wrong uploading the photo, please retry.",
);
},
async onSuccess(signedUrl) {
const { fileLocation } = await uploadFile(signedUrl, file);
if (!fileLocation) {
setUploadStatus("error");
return toast.error(
"Something went wrong uploading the photo, please retry.",
);
}
setUploadStatus("success");
setUploadUrl(fileLocation);
},
},
);
try {
const res = await getUploadUrl({
size,
type,
uploadType: "uploads",
});

const signedUrl = res?.data;

if (!signedUrl) {
setUploadStatus("error");
return toast.error(
"Something went wrong uploading the photo, please retry.",
);
}

const { fileLocation } = await uploadFile(signedUrl, file);
if (!fileLocation) {
setUploadStatus("error");
return toast.error(
"Something went wrong uploading the photo, please retry.",
);
}
setUploadStatus("success");
setUploadUrl(fileLocation);
} catch (error) {
setUploadStatus("error");
toast.error(
error instanceof Error
? error.message
: "An error occurred while uploading the image.",
);
Sentry.captureException(error);
}
}
};

Expand Down Expand Up @@ -162,8 +173,6 @@ const Create = ({ session }: { session: Session }) => {
},
);

const { mutate: getUploadUrl } = api.event.getUploadUrl.useMutation();

const PREVIEW_URL = `${process.env.NODE_ENV === "development" ? "http://localhost:3000" : "https://www.codu.co"}/draft/${postId}`;
const UPLOADED_IMAGE_URL = `![Image description](${uploadUrl})`;

Expand Down
44 changes: 44 additions & 0 deletions app/actions/getUploadUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use server";

import * as Sentry from "@sentry/nextjs";
import { getPresignedUrl } from "@/server/common/getPresignedUrl";
import { authActionClient } from "@/server/lib/safeAction";

import { z } from "zod";

const schema = z.object({
type: z.string(),
size: z.number(),
uploadType: z.enum(["uploads", "user"]).default("uploads"),
});

export const getUploadUrl = authActionClient
.schema(schema)
.action(async ({ parsedInput, ctx }) => {
const { type, size, uploadType } = parsedInput;
const extension = type.split("/")[1];
const acceptedFormats = ["jpg", "jpeg", "gif", "png", "webp"];

if (!acceptedFormats.includes(extension)) {
throw new Error(
`Invalid file. Accepted file formats: ${acceptedFormats.join(", ")}.`,
);
}

if (size > 1048576 * 10) {
throw new Error("Maximum file size 10mb");
}

try {
const response = await getPresignedUrl(type, size, {
kind: uploadType,
userId: ctx.user.id,
});

return response;
} catch (error) {
Sentry.captureException(error);
console.error("Error getting presigned URL:", error);
throw new Error("Failed to upload image.");
}
});
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"nanoid": "^5.0.7",
"next": "^14.2.10",
"next-auth": "^4.24.7",
"next-safe-action": "^7.9.4",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.14",
"pg": "^8.12.0",
Expand Down
3 changes: 1 addition & 2 deletions server/api/router/profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { user, emailChangeHistory } from "@/server/db/schema";
import { user } from "@/server/db/schema";
import {
saveSettingsSchema,
getProfileSchema,
Expand All @@ -20,7 +20,6 @@ import { emailTokenReqSchema } from "@/schema/token";
import { generateEmailToken, sendVerificationEmail } from "@/utils/emailToken";
import { TOKEN_EXPIRATION_TIME } from "@/config/constants";
import { emailChangeRequest } from "@/server/db/schema";
import { z } from "zod";

export const profileRouter = createTRPCRouter({
edit: protectedProcedure
Expand Down
15 changes: 15 additions & 0 deletions server/lib/safeAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import "server-only";

import { createSafeActionClient } from "next-safe-action";
import { getServerAuthSession } from "@/server/auth";
export const actionClient = createSafeActionClient();

export const authActionClient = actionClient.use(async ({ next }) => {
const session = await getServerAuthSession();

if (!session || !session.user) {
throw new Error("Session invalid.");
}

return next({ ctx: { user: session.user } });
});

0 comments on commit a47e8d2

Please sign in to comment.