From 0ab2ce2fccc1a1f6363a6b517d72c95759ddb4d8 Mon Sep 17 00:00:00 2001 From: Gillian Scott Date: Thu, 6 Mar 2025 20:34:11 -0500 Subject: [PATCH] gs-74 added functionality when user clicks create account, cleaned up user.service.ts file --- apps/backend/src/user/user.service.ts | 461 +++++++++--------- .../src/pages/registerPage/formPage.tsx | 43 +- 2 files changed, 275 insertions(+), 229 deletions(-) diff --git a/apps/backend/src/user/user.service.ts b/apps/backend/src/user/user.service.ts index 212aad34..027cc5f2 100644 --- a/apps/backend/src/user/user.service.ts +++ b/apps/backend/src/user/user.service.ts @@ -1,246 +1,265 @@ -import { Injectable } from "@nestjs/common"; -import { UserModel, EditUserModel } from "./user.model"; -import { DynamoDbService } from "../dynamodb"; -import { UserInputModel, UserStatus, Role } from "./user.model"; -import { NewUserInput } from "../dtos/newUserDTO"; +import { Injectable } from '@nestjs/common'; +import { UserModel, EditUserModel } from './user.model'; +import { DynamoDbService } from '../dynamodb'; +import { UserInputModel, UserStatus, Role } from './user.model'; +import { NewUserInput } from '../dtos/newUserDTO'; import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; @Injectable() export class UserService { + private readonly tableName = 'gibostonUsers'; + private readonly lambdaClient: LambdaClient = new LambdaClient({ + region: process.env.AWS_REGION, + }); + constructor(private readonly dynamoDbService: DynamoDbService) {} - private readonly tableName = 'gibostonUsers'; - private readonly lambdaClient: LambdaClient = new LambdaClient({ region: process.env.AWS_REGION }); - constructor(private readonly dynamoDbService: DynamoDbService) { } - - /** - * Gets a user's information based on the user's id. - * @param userId The user's id - * @throws Error if the user's data could not be fetched from DynamoDB - * @throws Error if the user's status or role is invalid - * @returns The user's information as a UserModel object - */ - public async getUser(userId: number): Promise { - try { - const key = { 'userId' : {N: userId.toString()}}; - const data = await this.dynamoDbService.getItem(this.tableName, key); - if (!data) { - throw new Error(`No user found with id: ${userId}`); - } - console.log(data); - return(this.mapDynamoDBItemToUserModel(userId,data)); - - } - catch(e) { - throw new Error(`Error fetching data for user with id: ${userId}: ${e.message}`); - } + /** + * Gets a user's information based on the user's id. + * @param userId The user's id + * @throws Error if the user's data could not be fetched from DynamoDB + * @throws Error if the user's status or role is invalid + * @returns The user's information as a UserModel object + */ + public async getUser(userId: number): Promise { + try { + const key = { userId: { N: userId.toString() } }; + const data = await this.dynamoDbService.getItem(this.tableName, key); + if (!data) { + throw new Error(`No user found with id: ${userId}`); + } + console.log(data); + return this.mapDynamoDBItemToUserModel(userId, data); + } catch (e) { + throw new Error( + `Error fetching data for user with id: ${userId}: ${e.message}`, + ); + } + } + public async postUser(userData: NewUserInput, role: Role) { + const newId = + (await this.dynamoDbService.getHighestUserId(this.tableName)) + 1; + const userModel = this.PostInputToUserVolunteerModel( + userData, + newId.toString(), + role, + ); + console.log('Received user data:', userData); + try { + const result = await this.dynamoDbService.postItem( + this.tableName, + userModel, + ); + return { ...result, newUserID: newId.toString() }; + } catch (e) { + throw new Error('Unable to post new user: ' + e); } + } - public async postUser(userData: NewUserInput, role: Role) { - const newId = await this.dynamoDbService.getHighestUserId(this.tableName) + 1; - const userModel = this.PostInputToUserVolunteerModel(userData, newId.toString(), role); - console.log("Received user data:", userData); - try { - const result = await this.dynamoDbService.postItem(this.tableName, userModel); - return {...result, newUserID: newId.toString()}; - } catch (e) { - throw new Error("Unable to post new user: " + e); - } + public async getUserTables(userId: number): Promise> { + try { + const key = { userId: { N: userId.toString() } }; + const data = await this.dynamoDbService.getItem(this.tableName, key); + if (!data) { + throw new Error(`No user found with id: ${userId}`); + } + console.log(data); + const siteIds = data['siteIds'].L.map((item) => Number(item.N)); + return siteIds; + } catch (e) { + throw new Error( + `Error fetching data for user with id: ${userId}: ${e.message}`, + ); } + } + + public async editUser(userId: number, model: EditUserModel): Promise { + try { + // A list of commands for each edit, only adds if exists + const commands = []; + const expressionAttributeValues: Record = {}; + const expressionAttributeNames: Record = {}; // Added for aliases + + const originalUser = await this.getUser(userId); + + if (model.firstName) { + commands.push(`firstName = :firstName`); + expressionAttributeValues[':firstName'] = { S: model.firstName }; + } + + if (model.lastName) { + commands.push(`lastName = :lastName`); + expressionAttributeValues[':lastName'] = { S: model.lastName }; + } + + if (model.phoneNumber) { + commands.push(`phoneNumber = :phoneNumber`); + expressionAttributeValues[':phoneNumber'] = { S: model.phoneNumber }; + } + + if (model.siteId) { + // Ensure siteId is treated as a string + const siteIdStr = model.siteId.toString(); + // Use list_append to add the new siteId to the siteIds list + commands.push(`siteIds = list_append(siteIds, :siteId)`); + expressionAttributeValues[':siteId'] = { L: [{ N: siteIdStr }] }; - public async getUserTables(userId: number): Promise> { - try { - const key = { 'userId' : {N: userId.toString()}}; - const data = await this.dynamoDbService.getItem(this.tableName, key); - if (!data) { - throw new Error(`No user found with id: ${userId}`); - } - console.log(data); - const siteIds = data["siteIds"].L.map(item => Number(item.N)); - return siteIds; - } - catch(e) { - throw new Error(`Error fetching data for user with id: ${userId}: ${e.message}`); + // Check if siteId exists in greenInfraBostonSites using getItem + const siteKey = { siteId: { S: siteIdStr } }; + const siteCheckResult = await this.dynamoDbService.getItem( + 'greenInfraBostonSites', + siteKey, + ); + + if (originalUser.siteIds.includes(model.siteId)) { + return { + statusCode: 400, + message: "Site already exists in user's siteIds", + }; } - } + if (!siteCheckResult) { + return { statusCode: 400, message: 'Site does not exist' }; + } + } - public async editUser(userId: number, model :EditUserModel): Promise { - - try { - // A list of commands for each edit, only adds if exists - const commands = []; - const expressionAttributeValues :Record = {}; - const expressionAttributeNames: Record = {}; // Added for aliases - - const originalUser = await this.getUser(userId); - - if (model.firstName) { - commands.push(`firstName = :firstName`); - expressionAttributeValues[":firstName"] = { S: model.firstName }; - } - - if (model.lastName) { - commands.push(`lastName = :lastName`); - expressionAttributeValues[":lastName"] = { S: model.lastName }; - } - - if (model.phoneNumber) { - commands.push(`phoneNumber = :phoneNumber`); - expressionAttributeValues[":phoneNumber"] = { S: model.phoneNumber }; - } - - if (model.siteId) { - // Ensure siteId is treated as a string - const siteIdStr = model.siteId.toString(); - - // Use list_append to add the new siteId to the siteIds list - commands.push(`siteIds = list_append(siteIds, :siteId)`); - expressionAttributeValues[":siteId"] = { L: [{ N: siteIdStr }] }; - - // Check if siteId exists in greenInfraBostonSites using getItem - const siteKey = { siteId: { S: siteIdStr } }; - const siteCheckResult = await this.dynamoDbService.getItem('greenInfraBostonSites', siteKey); - - if(originalUser.siteIds.includes(model.siteId)) { - return {statusCode: 400, message: "Site already exists in user's siteIds"}; - } - - if (!siteCheckResult) { - return {statusCode: 400, message: "Site does not exist"}; - } - } - - if (model.status) { - commands.push(`#s = :status`); - expressionAttributeValues[":status"] = { S: model.status }; - expressionAttributeNames["#s"] = "status"; - if (model.status === UserStatus.DENIED) { - // Send email if status is denied - const lamdaParams = { - FunctionName: 'giSendEmail', - Payload: JSON.stringify({ - firstName: model.firstName ? model.firstName : originalUser.firstName, - userEmail: originalUser.email - }), - } - const command = new InvokeCommand(lamdaParams); - await this.lambdaClient.send(command); - } - } - - - // Make sure commands aren't empty - if (commands.length === 0) { - throw new Error("No fields to update"); - } - - - // Combine into one update expression and update the user - const updateExpr = `SET ${commands.join(", ")}`; - const key = { 'userId' : {N: userId.toString()}}; - const data = await this.dynamoDbService.updateItemWithExpression(this.tableName, key, updateExpr,expressionAttributeValues, expressionAttributeNames) - - const result = await this.getUser(userId); - - - if (!data) { - return {statusCode: 400, message: "No user found with id: " + userId}; - } - return result; - } catch(e) { - throw new Error(`Error updating data for user with id: ${userId}: ${e.message}`); - } - } - - - // Run backend on postman please, not SwaggerUI - public async getUserByStatus(status: UserStatus): Promise { - try { - const filterExpression = "#user_status = :statusOf"; - const expressionAttributeValues = { ":statusOf": { S: status } }; - const expressionAttributeNames = {"#user_status":"status"} - - const data = await this.dynamoDbService.scanTable( - this.tableName, - filterExpression, - expressionAttributeValues, - expressionAttributeNames - - ); - - return data.map(item => this.mapDynamoDBItemToUserModelV2(item)); // added data - } catch (error) { - console.error("Error fetching users by status:", error); - throw new Error(`Error fetching users by status: ${error.message}`); + if (model.status) { + commands.push(`#s = :status`); + expressionAttributeValues[':status'] = { S: model.status }; + expressionAttributeNames['#s'] = 'status'; + if (model.status === UserStatus.DENIED) { + // Send email if status is denied + const lamdaParams = { + FunctionName: 'giSendEmail', + Payload: JSON.stringify({ + firstName: model.firstName + ? model.firstName + : originalUser.firstName, + userEmail: originalUser.email, + }), + }; + const command = new InvokeCommand(lamdaParams); + await this.lambdaClient.send(command); } - } + } + // Make sure commands aren't empty + if (commands.length === 0) { + throw new Error('No fields to update'); + } + // Combine into one update expression and update the user + const updateExpr = `SET ${commands.join(', ')}`; + const key = { userId: { N: userId.toString() } }; + const data = await this.dynamoDbService.updateItemWithExpression( + this.tableName, + key, + updateExpr, + expressionAttributeValues, + expressionAttributeNames, + ); - /** - * Maps a user's data from DynamoDB to a UserModel object. - * @param objectId the user's id - * @param data the user's data from DynamoDB - * @throws Error if the user's status or role is invalid - * @returns the user's information as a UserModel object - */ - private mapDynamoDBItemToUserModel(objectId: number, data?: {[key: string]: any}): UserModel { - const siteIds = Array.isArray(data["siteIds"]?.L) - ? data["siteIds"].L.map(item => Number(item.N)) - : []; - - - return { - userId: objectId, - firstName: data['firstName'].S, - lastName: data['lastName'].S, - email: data['email'].S, - phoneNumber: data['phoneNumber'].N, - siteIds: siteIds, - zipCode: data['zipCode'].S, - birthDate: new Date(data['birthDate'].S), - role: data['role'].S, - status: data['status'].S - }; - } + const result = await this.getUser(userId); - private PostInputToUserVolunteerModel = (input: NewUserInput, userId: string, role: Role): UserInputModel => { - return { - userId: {N: userId}, - firstName: {S: input.firstName}, - lastName: {S: input.lastName}, - phoneNumber: {S: input.phoneNumber}, - email: {S: input.email}, - zipCode: {S: input.zipCode}, - birthDate: {S: input.birthDate}, - role: {S: role}, - siteIds: { L: [] }, - status: {S: UserStatus.PENDING} - }; - } - /** - * Maps a user's data from DynamoDB to a UserModel object. - * @param data the user's data from DynamoDB - * @returns the user's information as a UserModel object - */ - private mapDynamoDBItemToUserModelV2(data: { [key: string]: any }): UserModel { - return { - userId: data["userId"].N, - firstName: data["firstName"].S, - lastName: data["lastName"].S, - email: data["email"].S, - phoneNumber: parseInt(data["phoneNumber"].N), - siteIds: data["siteIds"]?.NS?.map(Number) ?? [], - zipCode: parseInt(data["zipCode"].N), - birthDate: new Date(data["birthDate"].S), - role: data["role"].S, - status: data["status"].S as UserStatus, - }; + if (!data) { + return { statusCode: 400, message: 'No user found with id: ' + userId }; + } + return result; + } catch (e) { + throw new Error( + `Error updating data for user with id: ${userId}: ${e.message}`, + ); } + } + + // Run backend on postman please, not SwaggerUI + public async getUserByStatus(status: UserStatus): Promise { + try { + const filterExpression = '#user_status = :statusOf'; + const expressionAttributeValues = { ':statusOf': { S: status } }; + const expressionAttributeNames = { '#user_status': 'status' }; + const data = await this.dynamoDbService.scanTable( + this.tableName, + filterExpression, + expressionAttributeValues, + expressionAttributeNames, + ); + + return data.map((item) => this.mapDynamoDBItemToUserModelV2(item)); // added data + } catch (error) { + console.error('Error fetching users by status:', error); + throw new Error(`Error fetching users by status: ${error.message}`); + } + } + /** + * Maps a user's data from DynamoDB to a UserModel object. + * @param objectId the user's id + * @param data the user's data from DynamoDB + * @throws Error if the user's status or role is invalid + * @returns the user's information as a UserModel object + */ + private mapDynamoDBItemToUserModel( + objectId: number, + data?: { [key: string]: any }, + ): UserModel { + const siteIds = Array.isArray(data['siteIds']?.L) + ? data['siteIds'].L.map((item) => Number(item.N)) + : []; + return { + userId: objectId, + firstName: data['firstName'].S, + lastName: data['lastName'].S, + email: data['email'].S, + phoneNumber: data['phoneNumber'].N, + siteIds: siteIds, + zipCode: data['zipCode'].S, + birthDate: new Date(data['birthDate'].S), + role: data['role'].S, + status: data['status'].S, + }; + } + private PostInputToUserVolunteerModel = ( + input: NewUserInput, + userId: string, + role: Role, + ): UserInputModel => { + return { + userId: { N: userId }, + firstName: { S: input.firstName }, + lastName: { S: input.lastName }, + phoneNumber: { S: input.phoneNumber }, + email: { S: input.email }, + zipCode: { S: input.zipCode }, + birthDate: { S: input.birthDate }, + role: { S: role }, + siteIds: { L: [] }, + status: { S: UserStatus.PENDING }, + }; + }; + /** + * Maps a user's data from DynamoDB to a UserModel object. + * @param data the user's data from DynamoDB + * @returns the user's information as a UserModel object + */ + private mapDynamoDBItemToUserModelV2(data: { + [key: string]: any; + }): UserModel { + return { + userId: data['userId'].N, + firstName: data['firstName'].S, + lastName: data['lastName'].S, + email: data['email'].S, + phoneNumber: parseInt(data['phoneNumber'].N), + siteIds: data['siteIds']?.NS?.map(Number) ?? [], + zipCode: parseInt(data['zipCode'].N), + birthDate: new Date(data['birthDate'].S), + role: data['role'].S, + status: data['status'].S as UserStatus, + }; + } } diff --git a/apps/frontend/src/pages/registerPage/formPage.tsx b/apps/frontend/src/pages/registerPage/formPage.tsx index 59793fa1..b488356a 100644 --- a/apps/frontend/src/pages/registerPage/formPage.tsx +++ b/apps/frontend/src/pages/registerPage/formPage.tsx @@ -10,6 +10,7 @@ import { import { useState } from 'react'; import InfoIcon from '@mui/icons-material/Info'; import { Visibility, VisibilityOff } from '@mui/icons-material'; +import axios from 'axios'; interface FormPageProps { setIsSubmitted: (value: boolean) => void; @@ -18,6 +19,14 @@ interface FormPageProps { const FormPage: React.FC = ({ setIsSubmitted }) => { const [showPassword, setShowPassword] = useState(false); const [showRePassword, setShowRePassword] = useState(false); + const [userId, setUserId] = useState(''); + const [error, setError] = useState(''); + + const UserStatus = { + APPROVED: 'Approved', + PENDING: 'Pending', + DENIED: 'Denied', + }; const handleTogglePassword = () => { setShowPassword((prev) => !prev); @@ -27,6 +36,28 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { setShowRePassword((prev) => !prev); }; + const handleSubmit = async () => { + if (!userId.trim()) { + setError('User ID required'); + return; + } + //console.log('UserID: ', userId); + + try { + const response = await axios.put( + `${import.meta.env.VITE_API_BASE_URL}/users/editUser/${userId}`, + { status: UserStatus.APPROVED }, + ); + + setIsSubmitted(true); + + //console.log('Response:', response.data); + } catch (e) { + setError('Failed to approve user. Please try again.'); + console.error('Error: ', e); + } + }; + return ( <>

= ({ setIsSubmitted }) => { Volunteer Registration

- + = ({ setIsSubmitted }) => { fullWidth margin="none" size="small" + value={userId} + onChange={(e) => setUserId(e.target.value)} InputLabelProps={{ style: { fontFamily: 'Montserrat', fontWeight: 600 }, }} @@ -196,9 +225,7 @@ const FormPage: React.FC = ({ setIsSubmitted }) => { textTransform: 'none', marginTop: '40px', }} - onClick={() => { - setIsSubmitted(true); - }} + onClick={handleSubmit} > Create Account