diff --git a/__tests__/profileController.test.ts b/__refactor_tests__/profileController.test.ts similarity index 100% rename from __tests__/profileController.test.ts rename to __refactor_tests__/profileController.test.ts diff --git a/client/src/AuthenticatedApp.tsx b/client/src/AuthenticatedApp.tsx index b6d2321..0997689 100644 --- a/client/src/AuthenticatedApp.tsx +++ b/client/src/AuthenticatedApp.tsx @@ -5,6 +5,7 @@ import MainPage from "./pages/MainPage/MainPage"; import Forums from "./pages/Forums/Forums"; import Profiles from "./pages/Profiles/Profiles"; import Profile from "./pages/Profile/Profile"; +import EditProfilePage from "./pages/EditProfilePage/EditProfilePage"; import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; import { useNavigate } from "react-router-dom"; @@ -41,7 +42,8 @@ const AuthenticatedApp = () => { } /> } /> - } /> + } /> + } /> } /> } /> diff --git a/client/src/components/Banner/Banner.test.tsx b/client/src/components/Banner/Banner.test.tsx index f2ed51a..7d7e4dd 100644 --- a/client/src/components/Banner/Banner.test.tsx +++ b/client/src/components/Banner/Banner.test.tsx @@ -62,7 +62,7 @@ describe("Banner Component", () => { const optionsButton = screen.getByRole("button", { name: "Options" }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Go to Profile"); + const profileOption = screen.getByText("Edit Profile"); const logoutOption = screen.getByText("Logout"); expect(profileOption).toBeInTheDocument(); @@ -74,10 +74,10 @@ describe("Banner Component", () => { const optionsButton = screen.getByRole("button", { name: "Options" }); fireEvent.click(optionsButton); - const profileOption = screen.getByText("Go to Profile"); + const profileOption = screen.getByText("Edit Profile"); fireEvent.click(profileOption); - expect(mockNavigate).toHaveBeenCalledWith("profile"); + expect(mockNavigate).toHaveBeenCalledWith("editProfile"); }); it("handles logout on clicking Logout", () => { diff --git a/client/src/components/Banner/Banner.tsx b/client/src/components/Banner/Banner.tsx index 26da12c..3729191 100644 --- a/client/src/components/Banner/Banner.tsx +++ b/client/src/components/Banner/Banner.tsx @@ -16,8 +16,8 @@ const Banner = (): JSX.Element => { //TODO CLEAR ALL STATE }; - const goToProfile = () => { - navigate("profile"); + const goToEditProfile = () => { + navigate("editProfile"); setShowDropdown(false); }; return ( @@ -40,9 +40,9 @@ const Banner = (): JSX.Element => { - Go to Profile + Edit Profile { href="#!" className="block px-4 py-2 text-sm text-white hover:bg-gray-800" onClick={() => { - navigate("/app/profile"); + navigate("/app/editProfile"); setShowDropdown(false); }} > - Go to Profile + Edit Profile { }; return ( -
+
diff --git a/client/src/components/ProfileThumb/ProfileThumb.tsx b/client/src/components/ProfileThumb/ProfileThumb.tsx index 99d53c7..892b525 100644 --- a/client/src/components/ProfileThumb/ProfileThumb.tsx +++ b/client/src/components/ProfileThumb/ProfileThumb.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useAppDispatch } from "../../app/hooks"; import { IProfile } from "../../../types/profile"; interface ProfileThumbProps { @@ -7,12 +6,18 @@ interface ProfileThumbProps { } const ProfileThumb = ({ profile }: ProfileThumbProps): JSX.Element => { - const dispatch = useAppDispatch(); + const defaultImage = "https://picsum.photos/200"; return ( -
-

{profile.firstName}

-

{profile.bio}

+
+ {profile.fullName + +

{profile.fullName}

+

{profile.personalBio}

); }; diff --git a/client/src/features/userProfile/userProfileSlice.ts b/client/src/features/userProfile/userProfileSlice.ts index 0d67961..57f4543 100644 --- a/client/src/features/userProfile/userProfileSlice.ts +++ b/client/src/features/userProfile/userProfileSlice.ts @@ -4,7 +4,7 @@ import { IProfile } from "../../../types/profile"; export interface ProfileState { profile: IProfile | null; - status: "idle" | "loading" | "failed"; + status: "idle" | "loading" | "failed" | "updating"; error: string | null; } @@ -31,6 +31,52 @@ export const fetchUserProfile = createAsyncThunk( } ); +export const updateUserProfile = createAsyncThunk( + "profile/updateUserProfile", + async ( + { userID, ...updateData }: Partial & { userID: string }, + thunkAPI + ) => { + try { + const response = await axios.put(`/api/profiles/${userID}`, updateData); + return response.data; + } catch (error) { + let errorMessage = "An error occurred during profile update"; + if (axios.isAxiosError(error)) { + errorMessage = error.response?.data || errorMessage; + } + return thunkAPI.rejectWithValue(errorMessage); + } + } +); + +export const uploadProfilePicture = createAsyncThunk( + "profile/uploadProfilePicture", + async ( + { formData, userID }: { formData: FormData; userID: string }, + thunkAPI + ) => { + try { + const response = await axios.post( + `/api/images/profile-picture/${userID}`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + return response.data; + } catch (error) { + let errorMessage = "An error occurred during profile picture upload"; + if (axios.isAxiosError(error)) { + errorMessage = error.response?.data || errorMessage; + } + return thunkAPI.rejectWithValue(errorMessage); + } + } +); + const userProfileSlice = createSlice({ name: "profile", initialState, @@ -53,6 +99,28 @@ const userProfileSlice = createSlice({ .addCase(fetchUserProfile.rejected, (state, action) => { state.status = "failed"; state.error = action.payload as string; + }) + .addCase(updateUserProfile.pending, (state) => { + state.status = "updating"; + }) + .addCase(updateUserProfile.fulfilled, (state, action) => { + state.profile = action.payload; + state.status = "idle"; + }) + .addCase(updateUserProfile.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; + }) + .addCase(uploadProfilePicture.pending, (state) => { + state.status = "updating"; + }) + .addCase(uploadProfilePicture.fulfilled, (state, action) => { + state.profile = action.payload; + state.status = "idle"; + }) + .addCase(uploadProfilePicture.rejected, (state, action) => { + state.status = "failed"; + state.error = action.payload as string; }); }, }); diff --git a/client/src/pages/EditProfilePage/EditProfilePage.tsx b/client/src/pages/EditProfilePage/EditProfilePage.tsx new file mode 100644 index 0000000..3a07c94 --- /dev/null +++ b/client/src/pages/EditProfilePage/EditProfilePage.tsx @@ -0,0 +1,167 @@ +import React, { + useEffect, + useState, + useRef, + ChangeEvent, + FormEvent, +} from "react"; +import { useAppSelector, useAppDispatch } from "../../app/hooks"; +import { + fetchUserProfile, + updateUserProfile, + uploadProfilePicture, +} from "../../features/userProfile/userProfileSlice"; + +const EditProfilePage = () => { + const dispatch = useAppDispatch(); + const { profile, status } = useAppSelector((state) => state.userProfile); + const userID = useAppSelector((state) => state.user.userData?._id); + const fileInputRef = useRef(null); + + const [formData, setFormData] = useState({ + fullName: "", + email: "", + personalBio: "", + }); + + const [file, setFile] = useState(null); + + useEffect(() => { + if (userID) dispatch(fetchUserProfile(userID as string)); + }, [dispatch]); + + useEffect(() => { + if (profile) { + setFormData({ + fullName: profile.fullName || "", + email: profile.email || "", + personalBio: profile.personalBio || "", + }); + } + }, [profile]); + + const handleChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setFormData((prevFormData) => ({ + ...prevFormData, + [name]: value, + })); + }; + + const handleFileChange = (e: ChangeEvent) => { + const fileList = e.target.files; + if (!fileList) return; + setFile(fileList[0]); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!userID) { + console.error("UserID is undefined."); + return; + } + dispatch(updateUserProfile({ ...formData, userID })); + }; + + const handleImageUpload = () => { + if (!file || !userID) { + console.error("File or UserID is undefined."); + return; + } + + const formData = new FormData(); + formData.append("profilePicture", file); + + dispatch(uploadProfilePicture({ formData, userID })); + }; + + const handleFileInputClick = () => { + fileInputRef.current?.click(); + }; + + if (status === "loading" || !userID) { + return
Loading...
; + } + + return ( +
+

Edit Profile

+
+ + {profile?.profilePhoto && ( +
+ Profile +
+ )} + + + + + +
+

Upload Profile Picture

+ + {file &&

{file.name}

} + + +
+
+
+ ); +}; + +export default EditProfilePage; diff --git a/client/src/pages/LandingPage.tsx b/client/src/pages/LandingPage.tsx index 961de71..f36664b 100644 --- a/client/src/pages/LandingPage.tsx +++ b/client/src/pages/LandingPage.tsx @@ -3,11 +3,11 @@ import Login from "../components/Login/Login"; const LandingPage: React.FC = () => { return ( -
-

Code Hammers

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce - scelerisque iaculis libero. +

+

Code Hammers

+

+ Welcome to Code Hammers! Please log in to continue to the main + application.

diff --git a/client/src/pages/Profile/Profile.test.tsx b/client/src/pages/Profile/Profile.test.tsx index edd9cd3..2ae255d 100644 --- a/client/src/pages/Profile/Profile.test.tsx +++ b/client/src/pages/Profile/Profile.test.tsx @@ -4,29 +4,36 @@ import "@testing-library/jest-dom"; import Profile from "./Profile"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile } from "../../features/userProfile/userProfileSlice"; +import { useParams } from "react-router-dom"; jest.mock("../../app/hooks", () => ({ useAppDispatch: jest.fn(), useAppSelector: jest.fn(), })); +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useParams: jest.fn(), +})); + jest.mock("../../features/userProfile/userProfileSlice", () => ({ fetchUserProfile: jest.fn(), })); describe("Profile Component", () => { const mockDispatch = jest.fn(); - const mockUser = { - _id: "12345", - name: "John Doe", + const mockUserId = "123456"; + const mockUserProfile = { + user: mockUserId, + fullName: "John Doe", }; - //TODO MOCK BETTER USERPROFILE DATA + //TODO MOCK BETTER USERPROFILE DATA?? beforeEach(() => { (useAppDispatch as jest.Mock).mockReturnValue(mockDispatch); + (useParams as jest.Mock).mockReturnValue({ userId: mockUserId }); (useAppSelector as jest.Mock).mockImplementation((selector) => selector({ - user: { userData: mockUser }, - userProfile: { profile: null }, + userProfile: { profile: mockUserProfile }, }) ); }); @@ -43,12 +50,14 @@ describe("Profile Component", () => { it("dispatches fetchUserProfile on component mount", () => { render(); - expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUser._id)); + expect(mockDispatch).toHaveBeenCalledWith(fetchUserProfile(mockUserId)); }); - it("displays the user's ID", () => { + it("displays the user's fullName", () => { render(); - const userIdDisplay = screen.getByText(mockUser._id); - expect(userIdDisplay).toBeInTheDocument(); + + const userNameDisplay = screen.getByText(mockUserProfile.fullName); + + expect(userNameDisplay).toBeInTheDocument(); }); }); diff --git a/client/src/pages/Profile/Profile.tsx b/client/src/pages/Profile/Profile.tsx index 3a0cbed..6b7c238 100644 --- a/client/src/pages/Profile/Profile.tsx +++ b/client/src/pages/Profile/Profile.tsx @@ -1,20 +1,29 @@ import React, { useEffect } from "react"; +import { useParams } from "react-router-dom"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { fetchUserProfile } from "../../features/userProfile/userProfileSlice"; const Profile = (): JSX.Element => { const dispatch = useAppDispatch(); + const { userId } = useParams(); const userProfile = useAppSelector((state) => state.userProfile.profile); - const user = useAppSelector((state) => state.user.userData); useEffect(() => { - if (user?._id) dispatch(fetchUserProfile(user._id)); + console.log("userId", userId); + if (userId) dispatch(fetchUserProfile(userId)); }, [dispatch]); return ( -
-

Profile

-

{user?._id}

-

{userProfile?.bio}

+
+

Profile

+
+ Profile +

{userProfile?.fullName}

+

{userProfile?.personalBio}

+
); }; diff --git a/client/src/pages/Profiles/Profiles.test.tsx b/client/src/pages/Profiles/Profiles.test.tsx index b2b46d0..2966c7f 100644 --- a/client/src/pages/Profiles/Profiles.test.tsx +++ b/client/src/pages/Profiles/Profiles.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { create } from "react-test-renderer"; import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; +import { BrowserRouter } from "react-router-dom"; import Profiles from "./Profiles"; @@ -27,7 +28,9 @@ describe("MainPage Component", () => { const store = mockStore(initialState); const tree = create( - + + + ).toJSON(); expect(tree).toMatchSnapshot(); diff --git a/client/src/pages/Profiles/Profiles.tsx b/client/src/pages/Profiles/Profiles.tsx index 1aed94f..54b363b 100644 --- a/client/src/pages/Profiles/Profiles.tsx +++ b/client/src/pages/Profiles/Profiles.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { Link } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { fetchProfiles } from "../../features/profiles/profilesSlice"; import ProfileThumb from "../../components/ProfileThumb/ProfileThumb"; @@ -13,13 +14,20 @@ const Profiles = (): JSX.Element => { return ( <> -
-

PROFILES

-
-
- {profiles.map((profile) => ( - - ))} +
+

PROFILES

+ +
+ {profiles.map((profile, index) => ( + + + + ))} +
); diff --git a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap b/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap deleted file mode 100644 index 41700c4..0000000 --- a/client/src/pages/Profiles/__snapshots__/Profiles.test.tsx.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MainPage Component renders correctly 1`] = ` -[ -
-

- PROFILES -

-
, -
-
-

-

-

-
-

-

-

-
, -] -`; diff --git a/client/types/profile.ts b/client/types/profile.ts index 6911e62..f7dd63e 100644 --- a/client/types/profile.ts +++ b/client/types/profile.ts @@ -1,23 +1,71 @@ -interface ISocial { - linkedIn?: string; - github?: string; +import { Document, ObjectId } from "mongoose"; + +interface ISocialLinks { twitter?: string; - facebook?: string; - instagram?: string; + blog?: string; + other?: string[]; +} + +interface IProject { + name: string; + description?: string; + link?: string; } -interface IJob { +interface ICareerPosition { title?: string; company?: string; - description?: string; - date?: Date; + startDate?: Date; + endDate?: Date; +} + +interface IEducation { + institution: string; + degree?: string; + fieldOfStudy?: string; + startDate?: Date; + endDate?: Date; +} + +interface ITestimonial { + from: string; + relation?: string; + text: string; +} + +interface IBlogOrWriting { + title: string; + link: string; } -export interface IProfile { - user: string; - firstName: string; - lastName: string; - bio?: string; - job?: IJob; - socials?: ISocial; +export interface IProfile extends Document { + user: ObjectId; + fullName: string; + profilePhoto?: string; + cohort?: string; + graduationYear?: number; + email?: string; + linkedInProfile?: string; + professionalSummary?: string; + skills?: string[]; + specializations?: string[]; + careerInformation?: { + currentPosition?: { + title?: string; + company?: string; + }; + pastPositions?: ICareerPosition[]; + }; + education?: IEducation[]; + projects?: IProject[]; + personalBio?: string; + testimonials?: ITestimonial[]; + socialMediaLinks?: ISocialLinks; + availabilityForNetworking?: boolean; + bootcampExperience?: string; + achievementsAndCertifications?: string[]; + volunteerWork?: string[]; + eventParticipation?: string[]; + gallery?: string[]; + blogOrWriting?: IBlogOrWriting[]; } diff --git a/package-lock.json b/package-lock.json index 38eb51a..718bf55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "aws-sdk": "^2.1589.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", @@ -17,6 +18,7 @@ "html-webpack-plugin": "^5.5.3", "jsonwebtoken": "^9.0.1", "mongoose": "^7.3.4", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "webpack": "^5.88.1" }, @@ -30,6 +32,7 @@ "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.11", "@types/node": "^20.5.1", "@types/react": "^18.2.39", "@types/react-test-renderer": "^18.0.7", @@ -2896,6 +2899,15 @@ "mongoose": "*" } }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "20.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", @@ -3503,6 +3515,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -3561,7 +3578,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3569,6 +3585,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sdk": { + "version": "2.1589.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1589.0.tgz", + "integrity": "sha512-Tt3UHH6hoUEAjbCscqvfEAoq9VSTN5iSQO9XSisiiH/QJo8sf+iLCYmfJHM4tVkd92bQH61/xxj9t2Mazwc/WQ==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/axios": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", @@ -3716,6 +3769,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3864,6 +3936,16 @@ "node": ">=14.20.1" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3874,6 +3956,17 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4190,6 +4283,47 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/concurrently": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", @@ -4306,8 +4440,7 @@ "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/create-require": { "version": "1.1.1", @@ -5208,7 +5341,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -5488,7 +5620,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -5836,6 +5967,11 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -5944,7 +6080,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -6020,7 +6155,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6097,6 +6231,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6255,7 +6403,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -6303,8 +6450,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -7081,6 +7227,14 @@ "node": ">= 10.13.0" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7507,7 +7661,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7636,6 +7789,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -7818,7 +7999,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8172,8 +8352,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/prompts": { "version": "2.4.2", @@ -8256,6 +8435,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8753,6 +8941,11 @@ "node": ">=6" } }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -9250,6 +9443,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -9805,6 +10006,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -9918,6 +10124,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -9928,6 +10143,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -9937,11 +10157,22 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utila": { "version": "0.4.0", @@ -10479,7 +10710,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -10566,6 +10796,26 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -10576,7 +10826,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "engines": { "node": ">=0.4" } @@ -12775,6 +13024,15 @@ "mongoose": "*" } }, + "@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/node": { "version": "20.5.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz", @@ -13295,6 +13553,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -13349,8 +13612,36 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "aws-sdk": { + "version": "2.1589.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1589.0.tgz", + "integrity": "sha512-Tt3UHH6hoUEAjbCscqvfEAoq9VSTN5iSQO9XSisiiH/QJo8sf+iLCYmfJHM4tVkd92bQH61/xxj9t2Mazwc/WQ==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "dependencies": { + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" + }, + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } + } }, "axios": { "version": "1.6.2", @@ -13469,6 +13760,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -13583,6 +13879,16 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-5.4.0.tgz", "integrity": "sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==" }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -13593,6 +13899,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -13821,6 +14135,46 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "concurrently": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.0.tgz", @@ -13907,8 +14261,7 @@ "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "create-require": { "version": "1.1.1", @@ -14596,7 +14949,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -14794,7 +15146,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -15054,6 +15405,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -15134,7 +15490,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -15188,8 +15543,7 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { "version": "2.12.1", @@ -15233,6 +15587,14 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -15337,7 +15699,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, "requires": { "which-typed-array": "^1.1.11" } @@ -15370,8 +15731,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", @@ -15970,6 +16330,11 @@ "supports-color": "^8.0.0" } }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16301,8 +16666,7 @@ "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, "mkdirp": { "version": "1.0.4", @@ -16384,6 +16748,30 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -16527,8 +16915,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.12.3", @@ -16784,8 +17171,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "prompts": { "version": "2.4.2", @@ -16843,6 +17229,11 @@ "side-channel": "^1.0.4" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -17204,6 +17595,11 @@ "sparse-bitfield": "^3.0.3" } }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -17605,6 +18001,11 @@ "internal-slot": "^1.0.4" } }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -17972,6 +18373,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -18040,6 +18446,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + } + } + }, "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -18057,11 +18479,22 @@ "dev": true, "requires": {} }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "utila": { "version": "0.4.0", @@ -18446,7 +18879,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", - "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.4", @@ -18501,6 +18933,20 @@ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true }, + "xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -18510,8 +18956,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index bf9c22d..9ff156c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://github.com/Code-Hammers/code-hammers#readme", "dependencies": { + "aws-sdk": "^2.1589.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", @@ -35,6 +36,7 @@ "html-webpack-plugin": "^5.5.3", "jsonwebtoken": "^9.0.1", "mongoose": "^7.3.4", + "multer": "^1.4.5-lts.1", "react": "^18.2.0", "webpack": "^5.88.1" }, @@ -48,6 +50,7 @@ "@types/jest": "^29.5.3", "@types/jsonwebtoken": "^9.0.2", "@types/mongoose": "^5.11.97", + "@types/multer": "^1.4.11", "@types/node": "^20.5.1", "@types/react": "^18.2.39", "@types/react-test-renderer": "^18.0.7", diff --git a/server/controllers/imageController.ts b/server/controllers/imageController.ts new file mode 100644 index 0000000..8acbbb0 --- /dev/null +++ b/server/controllers/imageController.ts @@ -0,0 +1,76 @@ +import { Request, Response, NextFunction } from "express"; +import Profile from "../models/profileModel"; +import AWS from "aws-sdk"; + +AWS.config.update({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, +}); + +const s3 = new AWS.S3(); + +export const uploadProfilePicture = async ( + req: Request, + res: Response, + next: NextFunction +) => { + if (!req.file) { + return res.status(400).send("No file uploaded."); + } + + const { userID } = req.params; + + const file = req.file as Express.Multer.File; + const s3Key = `profile-pictures/${Date.now()}_${file.originalname}`; + + let params = { + Bucket: process.env.BUCKET_NAME as string, + Key: s3Key, + Body: file.buffer, + ContentType: file.mimetype, + ACL: "private", + }; + + console.log("params.Body", params.Body); + console.log(req.file); + + try { + const uploadResult = await s3.upload(params).promise(); + const updatedProfile = await Profile.findOneAndUpdate( + { user: userID }, + { profilePhoto: s3Key }, + { new: true } + ); + + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: s3Key, + Expires: 60 * 5, + }); + if (updatedProfile) { + updatedProfile.profilePhoto = presignedUrl; + } + res.status(201).send(updatedProfile); + } catch (err) { + console.error("Error uploading to S3:", err); + } +}; + +//TODO Currently not being used. Built into getProfileByID controller. +export const generatePresignedUrl = (req: Request, res: Response) => { + const key = req.query.key; + const params = { + Bucket: process.env.BUCKET_NAME, + Key: key as string, + Expires: 60, + }; + + s3.getSignedUrl("putObject", params, (err, url) => { + if (err) { + console.error("Error generating URL:", err); + return res.status(500).send("Error generating URL"); + } + res.status(200).send({ message: "URL generated successfully", url }); + }); +}; diff --git a/server/controllers/profileController.ts b/server/controllers/profileController.ts index 70e8928..1d284d0 100644 --- a/server/controllers/profileController.ts +++ b/server/controllers/profileController.ts @@ -1,6 +1,15 @@ import Profile from "../models/profileModel"; import { Request, Response, NextFunction } from "express"; import { IProfile } from "../types/profile"; +import AWS from "aws-sdk"; + +AWS.config.update({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION, +}); + +const s3 = new AWS.S3(); // ENDPOINT POST api/profiles/create // PURPOSE Create a new profile @@ -10,31 +19,75 @@ const createProfile = async ( res: Response, next: NextFunction ) => { - const { user, firstName, lastName, bio, job, socials } = req.body; + const { + user, + fullName, + profilePhoto, + cohort, + graduationYear, + email, + linkedInProfile, + professionalSummary, + skills, + specializations, + careerInformation, + education, + projects, + personalBio, + testimonials, + socialMediaLinks, + availabilityForNetworking, + bootcampExperience, + achievementsAndCertifications, + volunteerWork, + eventParticipation, + gallery, + blogOrWriting, + } = req.body; try { - const profile: IProfile = await Profile.create({ + const profile = await Profile.create({ user, - firstName, - lastName, - bio, - job, - socials, + fullName, + profilePhoto, + cohort, + graduationYear, + email, + linkedInProfile, + professionalSummary, + skills, + specializations, + careerInformation, + education, + projects, + personalBio, + testimonials, + socialMediaLinks, + availabilityForNetworking, + bootcampExperience, + achievementsAndCertifications, + volunteerWork, + eventParticipation, + gallery, + blogOrWriting, }); if (profile) { return res.status(201).json(profile); } } catch (error) { + console.error(error); return next({ log: "Express error in createProfile Middleware", status: 500, - message: { err: "An error occurred during profile creation" }, + message: { + err: "An error occurred during profile creation. Please try again.", + }, }); } }; -// ENDPOINT PATCH api/profiles/:UserID +// ENDPOINT PUT api/profiles/:UserID // PURPOSE Update an existing profile // ACCESS Private const updateProfile = async ( @@ -43,13 +96,12 @@ const updateProfile = async ( next: NextFunction ) => { const { userID } = req.params; - const { firstName, lastName, bio, job, socials } = req.body; + const { fullName, email, personalBio } = req.body; + const newProfile = { - firstName, - lastName, - bio, - job, - socials, + fullName, + email, + personalBio, }; try { @@ -95,7 +147,21 @@ const getAllProfiles = async ( message: { err: "There were no profiles to retrieve" }, }); } else { - return res.status(201).json(profiles); + const processedProfiles = await Promise.all( + profiles.map(async (profile) => { + if (profile.profilePhoto) { + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: profile.profilePhoto, + Expires: 60 * 5, + }); + profile.profilePhoto = presignedUrl; + } + return profile.toObject(); + }) + ); + + return res.status(201).json(processedProfiles); } } catch (error) { return next({ @@ -124,9 +190,17 @@ const getProfileById = async ( status: 404, message: { err: "An error occurred during profile retrieval" }, }); - } else { - return res.status(200).json(profile); } + if (profile.profilePhoto) { + const presignedUrl = s3.getSignedUrl("getObject", { + Bucket: process.env.BUCKET_NAME, + Key: profile.profilePhoto, + Expires: 60 * 5, + }); + profile.profilePhoto = presignedUrl; + } + + return res.status(200).json(profile); } catch (error) { return next({ log: "Express error in getProfileById Middleware", diff --git a/server/index.ts b/server/index.ts index a189f85..0250901 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,6 +3,7 @@ import express, { Request, Response, Application, NextFunction } from "express"; import userRoutes from "./routes/userRoutes"; import profileRoutes from "./routes/profileRoutes"; import authRoutes from "./routes/authRoutes"; +import imageRoutes from "./routes/imageRoutes"; import connectDB from "./config/db"; import dotenv from "dotenv"; import cookieParser from "cookie-parser"; @@ -20,6 +21,7 @@ connectDB(); app.use("/api/users", userRoutes); app.use("/api/profiles", profileRoutes); app.use("/api/auth", authRoutes); +app.use("/api/images", imageRoutes); console.log(`ENV BEFORE CHECK: ${process.env.NODE_ENV}`); diff --git a/server/models/profileModel.ts b/server/models/profileModel.ts index 308c38c..bfdd800 100644 --- a/server/models/profileModel.ts +++ b/server/models/profileModel.ts @@ -1,46 +1,74 @@ -import mongoose from "mongoose"; +import mongoose, { Schema } from "mongoose"; import { IProfile } from "../types/profile"; -const profileSchema = new mongoose.Schema({ - user: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - required: true, - }, - bio: { - type: String, - }, - firstName: { - type: String, - }, - lastName: { - type: String, - }, - job: { - title: String, - company: String, - description: String, - date: Date, - }, - socials: { - linkedIn: { - type: String, - }, - github: { - type: String, +const profileSchema = new Schema({ + user: { type: Schema.Types.ObjectId, ref: "User", required: true }, + fullName: { type: String, required: true }, + profilePhoto: String, + cohort: String, + graduationYear: Number, + email: String, + linkedInProfile: String, + professionalSummary: String, + skills: [String], + specializations: [String], + careerInformation: { + currentPosition: { + title: String, + company: String, }, - twitter: { - type: String, + pastPositions: [ + { + title: String, + company: String, + startDate: Date, + endDate: Date, + }, + ], + }, + education: [ + { + institution: String, + degree: String, + fieldOfStudy: String, + startDate: Date, + endDate: Date, }, - facebook: { - type: String, + ], + projects: [ + { + name: String, + description: String, + link: String, }, - instagram: { - type: String, + ], + personalBio: String, + testimonials: [ + { + from: String, + relation: String, + text: String, }, + ], + socialMediaLinks: { + twitter: String, + blog: String, + other: [String], }, + availabilityForNetworking: Boolean, + bootcampExperience: String, + achievementsAndCertifications: [String], + volunteerWork: [String], + eventParticipation: [String], + gallery: [String], + blogOrWriting: [ + { + title: String, + link: String, + }, + ], }); -const Profile = mongoose.model("Profile", profileSchema); +const Profile = mongoose.model("profiles", profileSchema); export default Profile; diff --git a/server/models/userModel.ts b/server/models/userModel.ts index ffcfcf4..f5aaadd 100644 --- a/server/models/userModel.ts +++ b/server/models/userModel.ts @@ -1,8 +1,8 @@ -import mongoose, { Document } from "mongoose"; +import mongoose, { Schema } from "mongoose"; import bcrypt from "bcryptjs"; import { IUser } from "../types/user"; -const userSchema = new mongoose.Schema({ +const userSchema = new Schema({ firstName: { type: String, required: true, diff --git a/server/routes/imageRoutes.ts b/server/routes/imageRoutes.ts new file mode 100644 index 0000000..1eb11a0 --- /dev/null +++ b/server/routes/imageRoutes.ts @@ -0,0 +1,23 @@ +import express from "express"; +import { + uploadProfilePicture, + generatePresignedUrl, +} from "../controllers/imageController"; +import { protect } from "../middleware/authMiddleware"; + +const router = express.Router(); + +import multer from "multer"; +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +router.post( + "/profile-picture/:userID", + upload.single("profilePicture"), + uploadProfilePicture +); + +//TODO Not currently being used +router.get("/generate-url", generatePresignedUrl); + +export default router; diff --git a/server/routes/profileRoutes.ts b/server/routes/profileRoutes.ts index 5b843d2..802c050 100644 --- a/server/routes/profileRoutes.ts +++ b/server/routes/profileRoutes.ts @@ -10,7 +10,7 @@ import { const router = express.Router(); router.post("/", createProfile); -router.patch("/:userID", updateProfile); +router.put("/:userID", updateProfile); router.get("/:userID", getProfileById); router.get("/", getAllProfiles); diff --git a/server/types/profile.ts b/server/types/profile.ts index b0e2056..f7dd63e 100644 --- a/server/types/profile.ts +++ b/server/types/profile.ts @@ -1,25 +1,71 @@ import { Document, ObjectId } from "mongoose"; -interface ISocial { - linkedIn?: string; - github?: string; +interface ISocialLinks { twitter?: string; - facebook?: string; - instagram?: string; + blog?: string; + other?: string[]; } -interface IJob { +interface IProject { + name: string; + description?: string; + link?: string; +} + +interface ICareerPosition { title?: string; company?: string; - description?: string; - date?: Date; + startDate?: Date; + endDate?: Date; +} + +interface IEducation { + institution: string; + degree?: string; + fieldOfStudy?: string; + startDate?: Date; + endDate?: Date; +} + +interface ITestimonial { + from: string; + relation?: string; + text: string; +} + +interface IBlogOrWriting { + title: string; + link: string; } export interface IProfile extends Document { user: ObjectId; - firstName: String; - lastName: String; - bio?: string; - job?: IJob; - socials?: ISocial; + fullName: string; + profilePhoto?: string; + cohort?: string; + graduationYear?: number; + email?: string; + linkedInProfile?: string; + professionalSummary?: string; + skills?: string[]; + specializations?: string[]; + careerInformation?: { + currentPosition?: { + title?: string; + company?: string; + }; + pastPositions?: ICareerPosition[]; + }; + education?: IEducation[]; + projects?: IProject[]; + personalBio?: string; + testimonials?: ITestimonial[]; + socialMediaLinks?: ISocialLinks; + availabilityForNetworking?: boolean; + bootcampExperience?: string; + achievementsAndCertifications?: string[]; + volunteerWork?: string[]; + eventParticipation?: string[]; + gallery?: string[]; + blogOrWriting?: IBlogOrWriting[]; }