From f3f97e8bbd4719eb98da0c29aa33992e5e7c0ed0 Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 10:43:52 +0545 Subject: [PATCH 1/6] feat: add uploadPhoto mutation to handle photo uploads --- .../user/src/model/users/graphql/resolver.ts | 63 +++++++++++++++++++ .../user/src/model/users/graphql/schema.ts | 4 ++ 2 files changed, 67 insertions(+) diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 51243a13e..84c602a4e 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -1,3 +1,4 @@ +import { GraphQLFileUpload, Multipart } from "@prefabs.tech/fastify-s3"; import { mercurius } from "mercurius"; import EmailVerification, { EmailVerificationClaim, @@ -306,6 +307,68 @@ const Mutation = { return mercuriusError; } }, + uploadPhoto: async ( + parent: unknown, + arguments_: { + photo: { + file: GraphQLFileUpload; + }; + }, + context: MercuriusContext, + ) => { + const { app, config, database, dbSchema, reply, user } = context; + const { photo } = arguments_; + + const service = getUserService(config, database, dbSchema); + + if (!user) { + return new mercurius.ErrorWithProps("unauthorized", {}, 401); + } + + try { + const fileData = photo.file.createReadStream(); + + const fileToUpload: Multipart = { + ...photo.file, + data: fileData, + limit: false, + }; + + const file = await service.uploadPhoto(fileToUpload, user.id, user.id); + + const updatedUser = await service.update(user.id, { + ...(file && { + photoId: file.id as number, + }), + }); + + if (user.photoId && user.photoId !== updatedUser.photoId) { + await service.fileService.delete(user.photoId); + } + + const request = reply.request; + + request.user = updatedUser; + + if (request.config.user.features?.profileValidation?.enabled) { + await request.session?.fetchAndSetClaim( + new ProfileValidationClaim(), + createUserContext(undefined, request), + ); + } + + return updatedUser; + } catch (error) { + app.log.error(error); + + const mercuriusError = new mercurius.ErrorWithProps( + "Oops, Something went wrong", + ); + mercuriusError.statusCode = 500; + + return mercuriusError; + } + }, removePhoto: async ( parent: unknown, arguments_: undefined, diff --git a/packages/user/src/model/users/graphql/schema.ts b/packages/user/src/model/users/graphql/schema.ts index 53ce4d5ff..4a9c00dc6 100644 --- a/packages/user/src/model/users/graphql/schema.ts +++ b/packages/user/src/model/users/graphql/schema.ts @@ -1,6 +1,8 @@ import { gql } from "@prefabs.tech/fastify-graphql"; const user = gql` + scalar Upload + type User { id: String! deletedAt: Float @@ -75,6 +77,8 @@ const user = gql` @auth(profileValidation: false, emailVerification: false) changeEmail(email: String!): ChangeEmailResponse @auth(profileValidation: false, emailVerification: false) + uploadPhoto(photo: Upload!): User + @auth(profileValidation: false, emailVerification: false) removePhoto: User @auth(profileValidation: false, emailVerification: false) } From 0247f790f7a9a42f3087ebb2142d03592b0be46a Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 13:59:41 +0545 Subject: [PATCH 2/6] feat: update multipart parser to handle all content types --- packages/s3/src/plugins/multipartParser.ts | 10 ++++++---- pnpm-lock.yaml | 11 +++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/s3/src/plugins/multipartParser.ts b/packages/s3/src/plugins/multipartParser.ts index 1746dd02a..21eb7fb6d 100644 --- a/packages/s3/src/plugins/multipartParser.ts +++ b/packages/s3/src/plugins/multipartParser.ts @@ -11,8 +11,10 @@ declare module "fastify" { } const plugin = async (fastify: FastifyInstance) => { - if (!fastify.hasContentTypeParser("multipart")) { - fastify.addContentTypeParser("multipart", (req, _payload, done) => { + fastify.addContentTypeParser("*", (req, _payload, done) => { + const contentType = req.headers["content-type"] || ""; + + if (contentType.includes("multipart")) { if ( req.config.graphql?.enabled && req.routeOptions.url?.startsWith(req.config.graphql.path as string) @@ -24,8 +26,8 @@ const plugin = async (fastify: FastifyInstance) => { } else { processMultipartFormData(req, _payload, done); } - }); - } + } + }); }; export default fastifyPlugin(plugin); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167776cab..bfa97b25c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4251,6 +4251,11 @@ packages: mercurius-auth@6.0.0: resolution: {integrity: sha512-pwxoF2xrYaItu9GFpNMFzNULmC7JCC3XCRzYtUOG8TgmrCtQ5N0SSKnA92FGkt/Q/5/3FfFJIiF4GSW6iRbolQ==} + mercurius-upload@8.0.0: + resolution: {integrity: sha512-lKYtqo6enNHRhFa6MrGE2OLf5/G0CvaXLCFp/Wczb6vS14LxtWor1fPwayNbiHV3RCS9DsiMnqCWdWaySZxKWA==} + peerDependencies: + graphql: ^16.3.0 + mercurius@16.1.0: resolution: {integrity: sha512-foMnqtFn3Wf/3zh0IJEz30Dq0alZIlIMCoQpxpS3/GkDB12h/LfmcoZ6ASCZ8xBJIvStEIKYxOyjfDeEcYhjLQ==} engines: {node: ^20.9.0 || >=22.0.0} @@ -10485,6 +10490,12 @@ snapshots: fastify-plugin: 5.0.1 graphql: 16.11.0 + mercurius-upload@8.0.0(graphql@16.11.0): + dependencies: + fastify-plugin: 5.0.1 + graphql: 16.11.0 + graphql-upload-minimal: 1.6.1(graphql@16.11.0) + mercurius@16.1.0(graphql@16.9.0): dependencies: '@fastify/error': 4.0.0 From 989f6cf5c3752a281126ff14f5e5af8185b7dc23 Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 14:07:52 +0545 Subject: [PATCH 3/6] feat: add email verification claim handling during user update --- packages/user/src/model/users/graphql/resolver.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 84c602a4e..5ab77a6ec 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -357,6 +357,13 @@ const Mutation = { ); } + if (request.config.user.features?.signUp?.emailVerification) { + await request.session?.fetchAndSetClaim( + EmailVerificationClaim, + createUserContext(undefined, request), + ); + } + return updatedUser; } catch (error) { app.log.error(error); From b7803eef7ee4d4c57b929646ac0df6a7021fc53e Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 14:11:22 +0545 Subject: [PATCH 4/6] feat: remove unused Upload scalar from user GraphQL schema --- packages/user/src/model/users/graphql/schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/user/src/model/users/graphql/schema.ts b/packages/user/src/model/users/graphql/schema.ts index 4a9c00dc6..810134a95 100644 --- a/packages/user/src/model/users/graphql/schema.ts +++ b/packages/user/src/model/users/graphql/schema.ts @@ -1,8 +1,6 @@ import { gql } from "@prefabs.tech/fastify-graphql"; const user = gql` - scalar Upload - type User { id: String! deletedAt: Float From 8e3a6e0ed2cb2a859ce3bdabdb42754d212bd0c5 Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 17:24:40 +0545 Subject: [PATCH 5/6] feat: add error handling for missing photo file in uploadPhoto mutation --- .../user/src/model/users/graphql/resolver.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 5ab77a6ec..5e8b1a00d 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -334,6 +334,14 @@ const Mutation = { limit: false, }; + if (!photo) { + throw new CustomApiError({ + message: "Missing photo file in the request body", + name: "ERROR_FILE_MISSING", + statusCode: 422, + }); + } + const file = await service.uploadPhoto(fileToUpload, user.id, user.id); const updatedUser = await service.update(user.id, { @@ -366,6 +374,14 @@ const Mutation = { return updatedUser; } catch (error) { + if (error instanceof CustomApiError) { + const mercuriusError = new mercurius.ErrorWithProps(error.name); + + mercuriusError.statusCode = error.statusCode; + + return mercuriusError; + } + app.log.error(error); const mercuriusError = new mercurius.ErrorWithProps( From b3da17dcee3f7254582c44686e5cfb6d35f17855 Mon Sep 17 00:00:00 2001 From: anvesh Date: Mon, 28 Jul 2025 17:28:08 +0545 Subject: [PATCH 6/6] feat: add error handling for missing photo file in uploadPhoto mutation --- .../user/src/model/users/graphql/resolver.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 5e8b1a00d..9db5903d8 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -325,6 +325,14 @@ const Mutation = { return new mercurius.ErrorWithProps("unauthorized", {}, 401); } + if (!photo) { + throw new CustomApiError({ + message: "Missing photo file in the request body", + name: "ERROR_FILE_MISSING", + statusCode: 422, + }); + } + try { const fileData = photo.file.createReadStream(); @@ -334,14 +342,6 @@ const Mutation = { limit: false, }; - if (!photo) { - throw new CustomApiError({ - message: "Missing photo file in the request body", - name: "ERROR_FILE_MISSING", - statusCode: 422, - }); - } - const file = await service.uploadPhoto(fileToUpload, user.id, user.id); const updatedUser = await service.update(user.id, {