-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
## Description This PR adds a new edit profile photo feature. Prior to this, RecNet displays a user's Google account profile photo and does not allow changes. This feature allows any RecNet user to upload a profile photo, which is stored in an AWS S3 bucket and make updates to the user profile information in the database. ## Frontend Changes * Modified `/apps/recnet/src/components/setting/profile/ProfileEditForm.tsx` adding new form element and function: * Added profile photo `<input>` element with a photo preview image to accept and display the selected photo. * Added `handleUploadS3` function to get a secure uploadUrl from the backend, put objects to the S3 bucket, and update the form data. * Modified the `onSubmit` function to call the `handleUploadS3` function when a user saves the form. * Updated the `ProfileEditSchema`, `useForm` hook to include the photoUrl field. * Added a `getUploadUrlMutation` hook to send a request to the backend to get a secure S3 upload URL. * Added new state hooks `[isUploading, setIsUploading]`, `[selectedFile, setSelectedFile]`, `[photoPreviewUrl, setPhotoPreviewUrl]` to manage the selected photo and upload status. * Created a new router `getS3UploadUrl` in the `/apps/recnet/src/server/routers/user.ts` * Modified the router `updateUser` in `/apps/recnet/src/server/routers/user.ts` to fetch the original photoUrl and delete the corresponding S3 file if a new profile photo is uploaded. ## Backend Changes * Added a new folder `s3` under `/apps/recnet-api/src/modules` * Created a `s3.controler.ts` to handle requests to the `s3url` endpoint. * Created a `s3.service.ts`. Included a `getS3UploadUrl` method and a `deleteS3Object` method. * Created a `s3.module.ts` to package and export the s3 class. * Modified `common.config.ts` under `/apps/recnet-api/src/config` to register AWS S3 configuration. * Updated `env.schema.ts` under /apps/recnet-api/src/config` to include the s3 environment variable data types. <!--- This project only accepts pull requests related to open issues --> <!--- If suggesting a new feature or change, please discuss it in an issue first --> <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> <!--- Please link to the issue here: --> ## Other Changes * Updated `app.module.ts` under `/apps/recnet-api/src` to import and include the `S3Module`. * Updated `package.json` to include `aws-sdk` . * Install aws-sdk: `@aws-sdk/client-s3 @aws-sdk/s3-request-presigner`. Make sure to use aws-sdk v3. <!-- Other thing to say --> ## How to Test 1. Log in to your account. 2. Navigate to the Profile page. 3. Click on the "Settings" button and open the first tab "Edit profile". 4. Click on "Select file" to choose a file and attach to the form. 5. Click on the "Save" button and wait for the upload to complete. 6. Check your profile photo and it should be updated to the new photo you uploaded. ## Screenshots (if appropriate): <img width="666" alt="edit_profile" src="https://github.com/user-attachments/assets/8edf4cdc-360e-4004-9f0d-bfb1fa99ca2f" /> <img width="668" alt="choose_photo" src="https://github.com/user-attachments/assets/10c96782-3774-4b48-945a-b8cb99bd3847" /> <img width="694" alt="uploading_photo" src="https://github.com/user-attachments/assets/b184154e-ea81-46d8-ad4d-7feaa52a814d" /> <img width="847" alt="profile_photo_updated" src="https://github.com/user-attachments/assets/22aa1810-f451-4c30-b80c-8d42aaea6575" /> ## TODO - Add AWS S3 configurations in the `.env` file under `/apps/recnet-api` for the dev and prod environment for deployment
- Loading branch information
Showing
15 changed files
with
1,708 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
apps/recnet-api/src/modules/photo-storage/photo-storage.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Controller, Delete, Post, Query } from "@nestjs/common"; | ||
import { ApiOperation } from "@nestjs/swagger"; | ||
|
||
import { PhotoStorageService } from "./photo-storage.service"; | ||
|
||
@Controller("photo-storage") | ||
export class PhotoStorageController { | ||
constructor(private readonly photoStorageService: PhotoStorageService) {} | ||
@ApiOperation({ | ||
summary: "Generate S3 Upload URL", | ||
description: | ||
"Generate a secure signed Url to upload profile photo to S3 bucket", | ||
}) | ||
@Post("upload-url") | ||
generateUploadUrl(): Promise<{ url: string }> { | ||
return this.photoStorageService.generateS3UploadUrl(); | ||
} | ||
|
||
@ApiOperation({ | ||
summary: "Delete S3 Object", | ||
description: "Delete S3 Object (profile photo)", | ||
}) | ||
@Delete("photo") | ||
async deleteS3Object(@Query("fileUrl") fileUrl: string): Promise<void> { | ||
return this.photoStorageService.deleteS3Object(fileUrl); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
apps/recnet-api/src/modules/photo-storage/photo-storage.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { Module } from "@nestjs/common"; | ||
|
||
import { PhotoStorageController } from "./photo-storage.controller"; | ||
import { PhotoStorageService } from "./photo-storage.service"; | ||
|
||
@Module({ | ||
controllers: [PhotoStorageController], | ||
providers: [PhotoStorageService], | ||
}) | ||
export class PhotoStorageModule {} |
83 changes: 83 additions & 0 deletions
83
apps/recnet-api/src/modules/photo-storage/photo-storage.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { S3Client } from "@aws-sdk/client-s3"; | ||
import { PutObjectCommand } from "@aws-sdk/client-s3"; | ||
import { DeleteObjectCommand } from "@aws-sdk/client-s3"; | ||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; | ||
import { HttpStatus, Inject, Injectable } from "@nestjs/common"; | ||
import { ConfigType } from "@nestjs/config"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
|
||
import { S3Config } from "@recnet-api/config/common.config"; | ||
import { RecnetError } from "@recnet-api/utils/error/recnet.error"; | ||
import { ErrorCode } from "@recnet-api/utils/error/recnet.error.const"; | ||
|
||
@Injectable() | ||
export class PhotoStorageService { | ||
private readonly s3: S3Client; | ||
|
||
constructor( | ||
@Inject(S3Config.KEY) | ||
private readonly s3Config: ConfigType<typeof S3Config> | ||
) { | ||
this.s3 = new S3Client({ | ||
region: this.s3Config.s3Region, | ||
credentials: { | ||
accessKeyId: this.s3Config.accessKeyId, | ||
secretAccessKey: this.s3Config.secretAccessKey, | ||
}, | ||
}); | ||
} | ||
|
||
async generateS3UploadUrl(): Promise<{ url: string }> { | ||
// build time stamp as part of the image name, format: YYYY-MM-DD-HH-MM-SS | ||
// e.g. the current date and time is February 21, 2025, 10:30:45 AM, | ||
// the timestamp would be: 2025-02-21-10-30-45 | ||
const timestamp = new Date() | ||
.toLocaleString("en-US", { | ||
year: "numeric", | ||
month: "2-digit", | ||
day: "2-digit", | ||
hour: "2-digit", | ||
minute: "2-digit", | ||
second: "2-digit", | ||
hour12: false, | ||
}) | ||
.replace(/[/,: ]/g, "-"); | ||
|
||
const imageName = `${timestamp}-${uuidv4()}`; | ||
|
||
const command = new PutObjectCommand({ | ||
Bucket: this.s3Config.bucketName, | ||
Key: imageName, | ||
}); | ||
|
||
try { | ||
const uploadURL = await getSignedUrl(this.s3, command, { expiresIn: 60 }); | ||
return { url: uploadURL }; | ||
} catch (error: unknown) { | ||
throw new RecnetError( | ||
ErrorCode.AWS_S3_GET_SIGNED_URL_ERROR, | ||
HttpStatus.INTERNAL_SERVER_ERROR | ||
); | ||
} | ||
} | ||
|
||
async deleteS3Object(fileUrl: string): Promise<void> { | ||
// Extract the key (filename) from the URL | ||
const urlParts = fileUrl.split("/"); | ||
const key = urlParts[urlParts.length - 1]; | ||
|
||
const command = new DeleteObjectCommand({ | ||
Bucket: this.s3Config.bucketName, | ||
Key: key, | ||
}); | ||
|
||
try { | ||
await this.s3.send(command); | ||
} catch (error: unknown) { | ||
throw new RecnetError( | ||
ErrorCode.AWS_S3_DELETE_OBJECT_ERROR, | ||
HttpStatus.INTERNAL_SERVER_ERROR | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.