diff --git a/backend/src/API/imageAPI.ts b/backend/src/API/imageAPI.ts index ef2b7e14a..ab5d43c6b 100644 --- a/backend/src/API/imageAPI.ts +++ b/backend/src/API/imageAPI.ts @@ -1,7 +1,41 @@ import { bucket } from '../firebase'; -import { getNetIDFromEmail, filterImagesResponse } from '../utils/memberUtil'; +import { filterImagesResponse } from '../utils/memberUtil'; import { NotFoundError } from '../utils/errors'; +/** + * Sets image for member + * @param name - the name of the image + * @returns a Promise to the signed URL to the image file + */ +export const getWriteSignedURL = async (name: string): Promise => { + const file = bucket.file(`${name}.jpg`); + const signedURL = await file.getSignedUrl({ + action: 'write', + version: 'v4', + expires: Date.now() + 15 * 60000 // 15 min + }); + return signedURL[0]; +}; + +/** + * Gets image for member + * @param name - the name of the image + * @throws NotFoundError if the requested image does not exist + * @returns a Promise to the signed URL to the image file + */ +export const getReadSignedURL = async (name: string): Promise => { + const file = bucket.file(`${name}.jpg`); + const fileExists = await file.exists().then((result) => result[0]); + if (!fileExists) { + throw new NotFoundError(`The requested image (${name}) does not exist`); + } + const signedURL = await file.getSignedUrl({ + action: 'read', + expires: Date.now() + 15 * 60000 + }); + return signedURL[0]; +}; + /** * Gets all profile images for members * @returns - an array of ProfileImage objects which includes file name and URL @@ -25,37 +59,10 @@ export const allMemberImages = async (): Promise => { }; /** - * Sets member image - * @param user - the member whose image will be set - * @returns - a Promise that represents the signedURL - */ -export const setMemberImage = async (user: IdolMember): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const file = bucket.file(`images/${netId}.jpg`); - const signedURL = await file.getSignedUrl({ - action: 'write', - version: 'v4', - expires: Date.now() + 15 * 60000 // 15 min - }); - return signedURL[0]; -}; - -/** - * Gets member image - * @param user - the requested member - * @returns - a Promise that represents signedURL which can be used to get the image file - * @throws NotFoundError if the requested image does not exist + * Deletes image for member + * @param name - the name of the image */ -export const getMemberImage = async (user: IdolMember): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const file = bucket.file(`images/${netId}.jpg`); - const fileExists = await file.exists().then((result) => result[0]); - if (!fileExists) { - throw new NotFoundError(`The requested image (${netId}.jpg) does not exist`); - } - const signedUrl = await file.getSignedUrl({ - action: 'read', - expires: Date.now() + 15 * 60000 - }); - return signedUrl[0]; +export const deleteImage = async (name: string): Promise => { + const imageFile = bucket.file(`${name}.jpg`); + await imageFile.delete(); }; diff --git a/backend/src/API/memberAPI.ts b/backend/src/API/memberAPI.ts index d4de64971..7fd2bb458 100644 --- a/backend/src/API/memberAPI.ts +++ b/backend/src/API/memberAPI.ts @@ -1,8 +1,8 @@ import MembersDao from '../dao/MembersDao'; import PermissionsManager from '../utils/permissionsManager'; import { BadRequestError, PermissionError } from '../utils/errors'; -import { bucket } from '../firebase'; import { getNetIDFromEmail, computeMembersDiff } from '../utils/memberUtil'; +import { deleteImage } from './imageAPI'; const membersDao = new MembersDao(); @@ -96,24 +96,12 @@ export const deleteMember = async (email: string, user: IdolMember): Promise - deleteImage(email).catch(() => { + await membersDao.deleteMember(email).then(() => { + const netId: string = getNetIDFromEmail(email); + deleteImage(`images/${netId}`).catch(() => { /* Ignore the error since the user might not have a profile picture. */ - }) - ); -}; - -/** - * Deletes the profile picture of an IDOL member given their email. - * @param email - the email of the member profile picture to delete. - */ -export const deleteImage = async (email: string): Promise => { - // Create a reference to the file to delete - const netId: string = getNetIDFromEmail(email); - const imageFile = bucket.file(`images/${netId}.jpg`); - - // Delete the file - await imageFile.delete(); + }); + }); }; /** diff --git a/backend/src/API/teamEventsImageAPI.ts b/backend/src/API/teamEventsImageAPI.ts deleted file mode 100644 index f1b2506b2..000000000 --- a/backend/src/API/teamEventsImageAPI.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { bucket } from '../firebase'; -import { getNetIDFromEmail } from '../utils/memberUtil'; -import { NotFoundError } from '../utils/errors'; - -/** - * Sets TEC proof image for member - * @param name - the name of the image - * @param user - the member who made the request - * @returns a Promise to the signed URL to the image file - */ -export const setEventProofImage = async (name: string, user: IdolMember): Promise => { - const file = bucket.file(`${name}.jpg`); - const signedURL = await file.getSignedUrl({ - action: 'write', - version: 'v4', - expires: Date.now() + 15 * 60000 // 15 min - }); - return signedURL[0]; -}; - -/** - * Gets TEC proof image for member - * @param name - the name of the image - * @param user - the member who made the request - * @throws NotFoundError if the requested image does not exist - * @returns a Promise to the signed URL to the image file - */ -export const getEventProofImage = async (name: string, user: IdolMember): Promise => { - const file = bucket.file(`${name}.jpg`); - const fileExists = await file.exists().then((result) => result[0]); - if (!fileExists) { - throw new NotFoundError(`The requested image (${name}) does not exist`); - } - const signedUrl = await file.getSignedUrl({ - action: 'read', - expires: Date.now() + 15 * 60000 - }); - return signedUrl[0]; -}; - -/** - * Gets all TEC proof images associated with the IdolMember - * @param user - the member who made the request - * @returns a Promise which results in an array of EventProofImage with file name and signed URL - */ -export const allEventProofImagesForMember = async ( - user: IdolMember -): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); - const images = await Promise.all( - files[0].map(async (file) => { - const signedURL = await file.getSignedUrl({ - action: 'read', - expires: Date.now() + 15 * 60000 // 15 min - }); - const fileName = await file.getMetadata().then((data) => data[1].body.name); - return { - fileName, - url: signedURL[0] - }; - }) - ); - - images - .filter((image) => image.fileName.length > 'eventProofs/'.length) - .map((image) => ({ - ...image, - fileName: image.fileName.slice(image.fileName.indexOf('/') + 1) - })); - - return images; -}; - -/** - * Deletes TEC proof image for member - * @param name - the name of the image - * @param user - the member who made the request - */ -export const deleteEventProofImage = async (name: string, user: IdolMember): Promise => { - const imageFile = bucket.file(`${name}.jpg`); - await imageFile.delete(); -}; - -/** - * Deletes all TEC proof images for given member - * @param user - the member who made the request - */ -export const deleteEventProofImagesForMember = async (user: IdolMember): Promise => { - const netId: string = getNetIDFromEmail(user.email); - const files = await bucket.getFiles({ prefix: `eventProofs/${netId}` }); - Promise.all( - files[0].map(async (file) => { - file.delete(); - }) - ); -}; diff --git a/backend/src/api.ts b/backend/src/api.ts index bea0ccd18..a0a37da38 100644 --- a/backend/src/api.ts +++ b/backend/src/api.ts @@ -24,7 +24,6 @@ import { reviewUserInformationChange, generateMemberArchive } from './API/memberAPI'; -import { getMemberImage, setMemberImage, allMemberImages } from './API/imageAPI'; import { allTeams, setTeam, deleteTeam } from './API/teamAPI'; import { getAllShoutouts, @@ -73,11 +72,6 @@ import { updateCandidateDeciderInstance, getCandidateDeciderReviews } from './API/candidateDeciderAPI'; -import { - deleteEventProofImage, - getEventProofImage, - setEventProofImage -} from './API/teamEventsImageAPI'; import { getAllDevPortfolios, createNewDevPortfolio, @@ -91,6 +85,7 @@ import { regradeSubmissions, updateSubmissions } from './API/devPortfolioAPI'; +import { getWriteSignedURL, getReadSignedURL, deleteImage } from './API/imageAPI'; import DPSubmissionRequestLogDao from './dao/DPSubmissionRequestLogDao'; import AdminsDao from './dao/AdminsDao'; import { sendMail } from './API/mailAPI'; @@ -253,16 +248,17 @@ loginCheckedPost('/team', async (req, user) => ({ })); // Images -loginCheckedGet('/member-image/:email', async (_, user) => ({ - url: await getMemberImage(user) +loginCheckedGet('/image/:name(*)', async (req) => ({ + url: await getReadSignedURL(req.params.name) })); -// TODO: Modify this endpoint to /member-image/* to be more RESTful -loginCheckedGet('/member-image-signedURL', async (_, user) => ({ - url: await setMemberImage(user) + +loginCheckedGet('/image-signed-url/:name(*)', async (req) => ({ + url: await getWriteSignedURL(req.params.name) })); -router.get('/member-image', async (_, res) => { - const images = await allMemberImages(); - res.status(200).json({ images }); + +loginCheckedDelete('/image/:name(*)', async (req) => { + await deleteImage(req.params.name); + return {}; }); // Shoutouts @@ -288,6 +284,7 @@ loginCheckedDelete('/shoutout/:uuid', async (req, user) => { return {}; }); +// Coffee Chats loginCheckedGet('/coffee-chat', async () => ({ coffeeChats: await getAllCoffeeChats() })); @@ -381,20 +378,6 @@ loginCheckedPost('/team-event-reminder', async (req, user) => ({ info: await notifyMember(req, req.query.end_of_semester_reminder !== undefined, req.body, user) })); -// Team Events Proof Image -loginCheckedGet('/event-proof-image/:name(*)', async (req, user) => ({ - url: await getEventProofImage(req.params.name, user) -})); - -// TODO: Modify this endpoint to /event-proof-image/* to be more RESTful -loginCheckedGet('/event-proof-image-signed-url/:name(*)', async (req, user) => ({ - url: await setEventProofImage(req.params.name, user) -})); -loginCheckedDelete('/event-proof-image/:name(*)', async (req, user) => { - await deleteEventProofImage(req.params.name, user); - return {}; -}); - // Candidate Decider loginCheckedGet('/candidate-decider', async (_, user) => ({ instances: await getAllCandidateDeciderInstances(user) diff --git a/common-types/index.d.ts b/common-types/index.d.ts index 561c5c3d1..e89844af9 100644 --- a/common-types/index.d.ts +++ b/common-types/index.d.ts @@ -116,7 +116,7 @@ interface TeamEvent extends TeamEventInfo { readonly requests: TeamEventAttendance[]; } -interface EventProofImage { +interface Image { readonly url: string; readonly fileName: string; } diff --git a/frontend/src/API/ImagesAPI.ts b/frontend/src/API/ImagesAPI.ts index 60608f59c..204215efc 100644 --- a/frontend/src/API/ImagesAPI.ts +++ b/frontend/src/API/ImagesAPI.ts @@ -3,11 +3,8 @@ import APIWrapper from './APIWrapper'; import HeadshotPlaceholder from '../static/images/headshot-placeholder.png'; export default class ImagesAPI { - // member images - public static getMemberImage(email: string): Promise { - const responseProm = APIWrapper.get(`${backendURL}/member-image/${email}`).then( - (res) => res.data - ); + public static getImage(name: string): Promise { + const responseProm = APIWrapper.get(`${backendURL}/image/${name}`).then((res) => res.data); return responseProm.then((val) => { if (val.error) { @@ -17,48 +14,21 @@ export default class ImagesAPI { }); } - private static getSignedURL(): Promise { - const responseProm = APIWrapper.get(`${backendURL}/member-image-signedURL`).then( - (res) => res.data - ); - return responseProm.then((val) => val.url); - } - - public static uploadMemberImage(body: Blob): Promise { - return this.getSignedURL().then((url) => { - const headers = { 'content-type': 'image/jpeg' }; - APIWrapper.put(url, body, headers).then((res) => res.data); - }); - } - - // Event proof images - public static getEventProofImage(name: string): Promise { - const responseProm = APIWrapper.get(`${backendURL}/event-proof-image/${name}`).then( - (res) => res.data - ); - return responseProm.then((val) => { - if (val.error) { - return HeadshotPlaceholder.src; - } - return val.url; - }); - } - - private static getEventProofImageSignedURL(name: string): Promise { - const responseProm = APIWrapper.get(`${backendURL}/event-proof-image-signed-url/${name}`).then( + private static getSignedURL(name: string): Promise { + const responseProm = APIWrapper.get(`${backendURL}/image-signed-url/${name}`).then( (res) => res.data ); return responseProm.then((val) => val.url); } - public static uploadEventProofImage(body: Blob, name: string): Promise { - return this.getEventProofImageSignedURL(name).then((url) => { - const headers = { 'content-type': 'image/jpeg' }; + public static uploadImage(body: Blob, name: string): Promise { + return this.getSignedURL(name).then((url) => { + const headers = { 'content-type': body.type }; APIWrapper.put(url, body, headers).then((res) => res.data); }); } - public static async deleteEventProofImage(name: string): Promise { - await APIWrapper.delete(`${backendURL}/event-proof-image/${name}`); + public static async deleteImage(name: string): Promise { + await APIWrapper.delete(`${backendURL}/image/${name}`); } } diff --git a/frontend/src/components/Admin/TeamEvent/TeamEventCreditReview.tsx b/frontend/src/components/Admin/TeamEvent/TeamEventCreditReview.tsx index b3bef5976..e830892aa 100644 --- a/frontend/src/components/Admin/TeamEvent/TeamEventCreditReview.tsx +++ b/frontend/src/components/Admin/TeamEvent/TeamEventCreditReview.tsx @@ -18,7 +18,7 @@ const TeamEventCreditReview = (props: { useEffect(() => { setLoading(true); - ImagesAPI.getEventProofImage(teamEventAttendance.image).then((url: string) => { + ImagesAPI.getImage(teamEventAttendance.image).then((url: string) => { setImage(url); setLoading(false); }); @@ -58,7 +58,7 @@ const TeamEventCreditReview = (props: { headerMsg: 'Team Event Attendance Rejected!', contentMsg: 'The team event attendance was successfully rejected!' }); - ImagesAPI.deleteEventProofImage(teamEventAttendance.image); + ImagesAPI.deleteImage(`${teamEventAttendance.image}`); Emitters.teamEventsUpdated.emit(); }) .catch((error) => { diff --git a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx index 71eb17db7..23ac743ad 100644 --- a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx +++ b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventCreditsForm.tsx @@ -54,7 +54,7 @@ const TeamEventCreditForm: React.FC = () => { const createdAttendance = await TeamEventsAPI.requestTeamEventCredit(eventCreditRequest); // upload image const blob = await fetch(uploadedImage).then((res) => res.blob()); - await ImagesAPI.uploadEventProofImage(blob, eventCreditRequest.image); + await ImagesAPI.uploadImage(blob, `${eventCreditRequest.image}`); return createdAttendance; }; diff --git a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventsCreditDashboard.tsx b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventsCreditDashboard.tsx index d402e8f0f..752182d75 100644 --- a/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventsCreditDashboard.tsx +++ b/frontend/src/components/Forms/TeamEventCreditsForm/TeamEventsCreditDashboard.tsx @@ -47,7 +47,7 @@ const TeamEventCreditDashboard = (props: { const getTeamEventImage = (attendance: TeamEventAttendance) => { setLoading(true); - ImagesAPI.getEventProofImage(attendance.image).then((url: string) => { + ImagesAPI.getImage(attendance.image).then((url: string) => { setImage(url); setLoading(false); }); @@ -63,7 +63,7 @@ const TeamEventCreditDashboard = (props: { headerMsg: 'Team Event Attendance Deleted!', contentMsg: 'Your team event attendance was successfully deleted!' }); - ImagesAPI.deleteEventProofImage(attendance.image); + ImagesAPI.deleteImage(`${attendance.image}`); Emitters.teamEventsUpdated.emit(); }) .catch((error) => { diff --git a/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx b/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx index 6682fd32a..a581ac969 100644 --- a/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx +++ b/frontend/src/components/Forms/UserProfile/UserProfileImage.tsx @@ -18,7 +18,7 @@ const UserProfileImage: React.FC = () => { if (process.env.NODE_ENV === 'test') { return; } - ImagesAPI.getMemberImage(userInfo ? userInfo.email : '').then((url: string) => { + ImagesAPI.getImage(`images/${userInfo ? userInfo.netid : ''}`).then((url: string) => { setProfilePhoto(url); }); }, [userInfo]); @@ -31,7 +31,7 @@ const UserProfileImage: React.FC = () => { .then((res) => res.blob()) .then((blob) => { imageURL = window.URL.createObjectURL(blob); - ImagesAPI.uploadMemberImage(blob); + ImagesAPI.uploadImage(blob, `images/${userInfo ? userInfo.netid : ''}`); setProfilePhoto(imageURL); }); }