User Profile Edit
Reading
User Profile Edit
Let us add a page and feature to allow the user to update their profile.
The first thing we will do is to create the component for editing a user. This
will look a lot like the SignUp.tsx
component.
Here are some notable differences:
- We add the
id
and thephotoURL
to the initialuseState
- We use
PUT
and a URL that includes the user's id - When we get a positive API response, we call a method in
auth.js
namedupdateUserAuth
that updates the local storage. - When we get a positive API response, we use
window.location
to force a full page refresh, so we fetch fresh auth information to display (name and avatar)
EditUser.tsx
import React, { useState } from 'react'import { Link, useHistory } from 'react-router-dom'import { useDropzone } from 'react-dropzone'import { authHeader, getUser, updateUserAuth } from '../auth'export function EditUser() {const history = useHistory()const user = getUser()const [errorMessage, setErrorMessage] = useState()const [updatedUser, setUpdatedUser] = useState({id: user.id,fullName: user.fullName,email: user.email,password: '',photoURL: user.photoURL,})const { getRootProps, getInputProps, isDragActive } = useDropzone({onDrop: onDropFile,})const [isUploading, setIsUploading] = useState(false)function handleStringFieldChange(event) {const value = event.target.valueconst fieldName = event.target.nameconst newUpdatedUser = { ...updatedUser, [fieldName]: value }setUpdatedUser(newUpdatedUser)}async function handleFormSubmit(event) {event.preventDefault()const response = await fetch(`/api/Users/${user.id}`, {method: 'PUT',headers: { 'content-type': 'application/json', ...authHeader() },body: JSON.stringify(updatedUser),})const apiResponse = await response.json()if (apiResponse.status === 400) {setErrorMessage(Object.values(apiResponse.errors).join(' '))} else {updateUserAuth(apiResponse)window.location.assign('/')}}async function onDropFile(acceptedFiles) {// Do something with the filesconst fileToUpload = acceptedFiles[0]console.log(fileToUpload)// Create a formData object so we can send this// to the API that is expecting some form data.const formData = new FormData()// Append a field that is the form upload itselfformData.append('file', fileToUpload)try {setIsUploading(true)// Use fetch to send an authorization header and// a body containing the form data with the fileconst response = await fetch('/api/Uploads', {method: 'POST',headers: {...authHeader(),},body: formData,})setIsUploading(false)// If we receive a 200 OK response, set the// URL of the photo in our state so that it is// sent along when creating the restaurant,// otherwise show an errorif (response.status === 200) {const apiResponse = await response.json()const url = apiResponse.urlsetUpdatedUser({ ...updatedUser, photoURL: url })} else {setErrorMessage('Unable to upload image')}} catch (error) {// Catch any network errors and show the user we could not process their uploadconsole.debug(error)setErrorMessage('Unable to upload image')setIsUploading(false)}}let dropZoneMessage = 'Drag a picture of yourself here!'if (isUploading) {dropZoneMessage = 'Uploading...'}if (isDragActive) {dropZoneMessage = 'Drop the files here ...'}return (<main className="page"><nav><Link to="/"><i className="fa fa-home"></i></Link><h2>Edit Profile</h2></nav><form onSubmit={handleFormSubmit}>{errorMessage ? <p>{errorMessage}</p> : null}<p className="form-input"><label htmlFor="fullName">Name</label><inputtype="text"name="fullName"value={updatedUser.fullName}onChange={handleStringFieldChange}/></p><p className="form-input"><label htmlFor="name">Email</label><inputtype="email"name="email"value={updatedUser.email}onChange={handleStringFieldChange}/></p><p className="form-input"><label htmlFor="password">Password</label><inputtype="password"name="password"value={updatedUser.password}onChange={handleStringFieldChange}/></p>{updatedUser.photoURL ? (<p><img alt="User Photo" width={200} src={updatedUser.photoURL} /></p> : null)}<div className="file-drop-zone"><div {...getRootProps()}><input {...getInputProps()} />{dropZoneMessage}</div></div><p><input type="submit" value="Submit" /></p></form></main>)}
App.tsx changes
We'll add a profile link in the navigation.
{isLoggedIn() ? <Link to="/profile">Profile</Link> : null}
and a route
<Route exact path="/profile"><EditUser /></Route>
UsersController changes
We add back in a Put
method. The first difference in this version of a PUT
is that we make sure to make this an authorized endpoint. Next, we ensure the
user id is equal to the current user's id. This prevents someone from editing a
profile that is not theirs.
// PUT: api/Users/5//// Update an individual User with the requested id. The id is specified in the URL// In the sample URL above it is the `5`. The "{id} in the [HttpPut("{id}")] is what tells dotnet// to grab the id from the URL. It is then made available to us as the `id` argument to the method.//// In addition the `body` of the request is parsed and then made available to us as a User// variable named User. The controller matches the keys of the JSON object the client// supplies to the names of the attributes of our User POCO class. This represents the// new values for the record.//[HttpPut("{id}")][Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]public async Task<IActionResult> PutUser(int id, User User){// If the ID in the URL does not match the ID in the supplied request body, return a bad requestif (id != User.Id){return BadRequest();}if (id != GetCurrentUserId()){return BadRequest();}// Tell the database to consider everything in User to be _updated_ values. When// the save happens the database will _replace_ the values in the database with the ones from User_context.Entry(User).State = EntityState.Modified;try{// Try to save these changes.await _context.SaveChangesAsync();}catch (DbUpdateConcurrencyException){// Ooops, looks like there was an error, so check to see if the record we were// updating no longer exists.if (!UserExists(id)){// If the record we tried to update was already deleted by someone else,// return a `404` not foundreturn NotFound();}else{// Otherwise throw the error back, which will cause the request to fail// and generate an error to the client.throw;}}// return NoContent to indicate the update was done. Alternatively you can use the// following to send back a copy of the updated data.//return Ok(User);}// Private helper method that looks up an existing User by the supplied idprivate bool UserExists(int id){return _context.Users.Any(User => User.Id == id);}// Private helper method to get the JWT claim related to the user IDprivate int GetCurrentUserId(){// Get the User Id from the claim and then parse it as an integer.return int.Parse(User.Claims.FirstOrDefault(claim => claim.Type == "Id").Value);}
Future improvement
We allow the user to update their password without confirming their existing password. This is a security risk if someone leaves their computer alone. Anyone who has access can change the password without confirmation. We also allow the email address to change without any confirmation.
Both of these should have logic in place to make sure someone doesn't hijack our accounts.