diff --git a/.gitignore b/.gitignore index 1b5d52f..e8f77c2 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,7 @@ server.exe # debug and cached routes /src/routes.ts -/src/modules/debug \ No newline at end of file +/src/modules/debug + +# uploaded files +/uploads \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 1066575..c7e7dac 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 0837bc7..54a28cd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@prisma/client": "^6.7.0", "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.9", + "@types/mime-types": "^3.0.1", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", "cookie": "^1.0.2", @@ -20,6 +21,7 @@ "ioredis": "^5.6.1", "joi": "^17.13.3", "jsonwebtoken": "^9.0.2", + "mime-types": "^3.0.1", "mock-aws-s3": "^4.0.2", "nock": "^14.0.4", "ua-parser-js": "^2.0.3" diff --git a/src/constants/file b/src/constants/file new file mode 100644 index 0000000..e69de29 diff --git a/src/helpers/files/saveFile/index.ts b/src/helpers/files/saveFile/index.ts new file mode 100644 index 0000000..9457096 --- /dev/null +++ b/src/helpers/files/saveFile/index.ts @@ -0,0 +1,26 @@ +import { mkdir, writeFile } from "fs/promises"; +import path from "path"; +import crypto from "crypto"; +import mime from "mime-types"; + +interface SaveFileOptions { + folder: string; + prefix?: string; +} + +export const saveFile = async ( + file: File, + { folder, prefix }: SaveFileOptions +): Promise => { + const ext = mime.extension(file.type) || "bin"; + const uniqueName = `${prefix ?? ""}${crypto.randomUUID()}.${ext}`; + + const relativeFolder = path.join("uploads", folder); + const relativePath = path.join(relativeFolder, uniqueName); + const absolutePath = path.join(process.cwd(), relativePath); + + await mkdir(path.dirname(absolutePath), { recursive: true }); + await writeFile(absolutePath, Buffer.from(await file.arrayBuffer())); + + return relativePath; +}; diff --git a/src/helpers/files/saveFile/modules/saveAvatar.ts b/src/helpers/files/saveFile/modules/saveAvatar.ts new file mode 100644 index 0000000..b09492a --- /dev/null +++ b/src/helpers/files/saveFile/modules/saveAvatar.ts @@ -0,0 +1,17 @@ +import { saveFile } from ".."; +import { AppError } from "../../../error/instances/app"; + +export const saveAvatar = async (file: File): Promise => { + const allowedTypes = ["image/png", "image/jpeg", "image/webp"]; + if (!allowedTypes.includes(file.type)) { + throw new AppError( + 415, + "Unsupported Media Type. File must be in .jpg, .png, or .webp format" + ); + } + + return await saveFile(file, { + folder: "avatar", + prefix: "usr-", + }); +}; diff --git a/src/modules/user/services/editUser.service.ts b/src/modules/user/services/editUser.service.ts index 4ea4b58..e749bfe 100644 --- a/src/modules/user/services/editUser.service.ts +++ b/src/modules/user/services/editUser.service.ts @@ -7,6 +7,7 @@ import { checkUserEmailAndUsernameAvailabillityService } from "./checkUserEmailA import { logoutService } from "../../auth/services/logout.service"; import { loginFromSystemService } from "../../auth/services/loginFromSystem.service"; import { UserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation/types"; +import { saveAvatar } from "../../../helpers/files/saveFile/modules/saveAvatar"; export const editUserService = async ( cookie: string, @@ -29,6 +30,11 @@ export const editUserService = async ( "The username or email has already taken by another user." ); + // Store the avatar to the file system if provided in the payload + let storeAvatar: string | undefined = undefined; + if (payload.profilePicture) + storeAvatar = await saveAvatar(payload.profilePicture as File); + // Prepare the fields to update, only include fields that are provided in the payload const fieldsToUpdate: Partial = { ...(payload.username && payload.username !== jwtSession.user.username @@ -47,9 +53,7 @@ export const editUserService = async ( ...(payload.bioProfile !== undefined ? { bioProfile: payload.bioProfile } : {}), - ...(payload.profilePicture !== undefined - ? { profilePicture: payload.profilePicture } - : {}), + ...(storeAvatar !== undefined ? { profilePicture: storeAvatar } : {}), ...(payload.commentPicture !== undefined ? { commentPicture: payload.commentPicture } : {}), @@ -61,6 +65,7 @@ export const editUserService = async ( // Update the user in the database, use username from the JWT session to find the user await updateUserRepo(jwtSession.user.username, fieldsToUpdate); + // Clear the session and re-login the user to get a new JWT token await logoutService(cookie); const newUserSession = await loginFromSystemService( jwtSession.userId,