'How can I pass both the title and an image file from my front end (React.js) to my backend API (Spring Boot)?
I am attempting to make an application that allows a registered user to submit an image file (.jpeg, .png, etc) along with a title of that image. I am having trouble wrapping my mind around how to do this.
I need to send the image to an amazon AWS S3 bucket, and I was able to figure out how to do this, but adding a title input has confused me a lot in terms of how to get both the file and title from my front-end (JSON) to my back-end API and have it be saved to my post database (JPA). I have a user_post database that has the following column: ID (post ID primary key), user_id (foreign key from User table), title, & post_image (containing the file that is to be saved in the DB). So basically, I need to figure out how to get the input (both file and title) at once, send it to my back-end and save it in the Post DB.
Do I need to pass the title as an argument in my post controller? Since the UserPost DB is a one to many relationship with a User, I need to pass that logged in user's ID to my controller so that it gets sent to the post DB, I'm just not sure how.
I have been searching online everywhere for help and figured I would give Stack Overflow a shot on this as I desperately need help. I am using JWT authentication through local storage for now to access user information such as username and id. Before I was testing my API with Postman by using the "file" key in the body and then proceeding to choose the image that is to be selected, but how can I do this if I want to add a title as well? Ideally, it would be the same but with text instead of file for the type as seen below but I am not sure. (Of course depending on what key I set for the title if I am thinking of it right).
This is a screenshot of my front-end just for a visual in case it helps at all.
Here is my code, I apologize for it being so messy, I am working on cleaning it up as soon as I can. I will start with the front-end. I believe I need to set the key for my title input so that it gets passed to my back-end API, but I am not even sure if I am doing that right. I also needed to use a custom form validation since I am dealing with an image upload. I ended up putting the call to my API in my useForm.js file since my form validations are done in that custom hook.
Upload.jsx:
import '../../components/pages/styles/Uploads.css';
import {Formik, Field, Form, ErrorMessage} from 'formik';
import * as Yup from 'yup';
import {useEffect, useState} from 'react';
import {} from 'react-router-dom';
import {useDispatch, useSelector} from 'react-redux';
import axios from 'axios';
import {clearMessage} from '../../slices/messages';
import authHeader from '../../services/auth-header';
import useForm from '../../hooks/useForm';
const API_URL = 'http://localhost:8080/api/posts';
function Uploads(onUpload) {
const {user: currentUser} = useSelector((state) => state.auth);
const [file, setFile] = useState();
const [title, setTitle] = useState();
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(false);
const [content, setContent] = useState('');
const [preview, setPreview] = useState(null);
const initialValues = {
title: '',
};
const [formErrors, setFormErrors] = useState();
useEffect(() => {}, []);
const handlePost = (formValue) => {};
const onAddImage = (file) => {
window.URL.revokeObjectURL(preview);
if (!file) return;
setPreview(window.URL.createObjectURL(file));
};
//The postUserImage function below was moved to useForm.js
//since my form validations are done there. I might have
//messed this up.
// const postUserImage = async (event) => {
//This is the code that will get passed to my backend. I need the image and title to be added here somehow.
// event.preventDefault();
// const formData = new FormData();
// formData.append('file', file);
// formData.append('title', title);
// const result = await axios.post('/upload', formData, {
// headers: {...authHeader(), 'Content-Type': 'multipart/form-data'},
// });
// console.log(result.data);
// };
//Custom hook call
// const {handleChange, values, errors, handleSubmit} = useForm(postUserImage);
const initialState = {title: ''};
const validations = [
({title}) => isRequired(title) || {title: 'Title is required'},
];
const {values, isValid, errors, changeHandler, submitHandler, touched} =
useForm(initialState, validations, onUpload);
return (
<div className='page'>
<div className='upload-card'>
<div id='preview'>
<img
src={preview || require('../../assets/user-solid.jpeg')}
id='image'
alt='Thumbnail'
className='user-post'
/>
</div>
</div>
<div className='upload-container'>
<div className='post-form-container'>
<p id='upload-form-label'>Hello, feel free to post an image!</p>
<form
// onSubmit={'return Validate(this);'}
onSubmit={submitHandler}
className='upload-form'
>
<div className='panel'>
<div className='button_outer'>
<div className='btn_upload'>
<input
filename={file}
onChange={(e) => onAddImage(e.target.files[0])}
type='file'
accept='.jpeg,.svg,.gif,.png'
id='image-selection-btn'
></input>
Choose your Art
</div>
</div>
</div>
<input
name='title'
type='text'
className='form-control'
placeholder='Enter Title'
id='cred-input'
required
value={values.title}
onChange={changeHandler}
/>
{touched.title && errors.title && (
<p className='error'>{errors.title}</p>
)}
<button type='submit' id='post-upload-btn' disabled={isValid}>
Upload Image
</button>
</form>
</div>
</div>
</div>
);
function isRequired(value) {
return value != null && value.trim().length > 0;
}
function isSame(value1, value2) {
return value1 === value2;
}
}
export default Uploads;
useForm.js:
import React, {useState} from 'react';
import {omit} from 'lodash';
import authHeader from '../services/auth-header';
import axios from 'axios';
function useForm(initialState = {}, validations = [], onSubmit = () => {}) {
const API_URL = 'http://localhost:8080/api/posts';
// Add the 'onSubmit' argument
const {isValid: initialIsValid, errors: initialErrors} = validate(
validations,
initialState
);
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState(initialErrors);
const [isValid, setValid] = useState(initialIsValid);
const [touched, setTouched] = useState({});
const [file, setFile] = useState();
const [title, setTitle] = useState();
const changeHandler = (event) => {
const newValues = {...values, [event.target.name]: event.target.value};
const {isValid, errors} = validate(validations, newValues);
setValues(newValues);
setValid(isValid);
setErrors(errors);
setTouched({...touched, [event.target.name]: true});
};
// Add this
const submitHandler = async (event) => {
event.preventDefault();
onSubmit(values);
const formData = new FormData();
formData.append('file', file);
formData.append('title', values.title);
const result = await axios.post('/upload', formData, {
headers: {...authHeader(), 'Content-Type': 'multipart/form-data'},
});
console.log(result.data);
};
return {values, changeHandler, isValid, errors, touched, submitHandler}; // Add 'submitHandler'
}
function validate(validations, values) {
const errors = validations
.map((validation) => validation(values))
.filter((validation) => typeof validation === 'object');
return {
isValid: errors.length === 0,
errors: errors.reduce((errors, error) => ({...errors, ...error}), {}),
};
}
export default useForm;
Back-end code:
User.java:
package com.Application.models;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table( name = "users",
uniqueConstraints = {
@UniqueConstraint(columnNames = "username"),
})
public class User {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
@NotBlank
@Size(max = 20)
private String username;
@Column(name = "email")
@NotBlank
@Size(max = 50)
@Email
private String email;
@NotBlank
@Size(max = 120)
private String password;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable( name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "user")
@Column(name = "user_post")
private Set<UserPosts> userPosts = new HashSet<>();
public User(String username, String email
,String password) {
this.username = username;
this.email = email;
this.password = password;
}
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
UserRepository.java:
package com.Application.repository;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.HashTek.HashTekApplication.models.User;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
UserPost.java:
package com.Application.models;
import javax.persistence.*;
@Entity
@Table(name = "user_posts")
public class UserPosts {
@Id
@Column(name = "id")
private Long id;
@ManyToOne(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "title")
private String title;
@Column(name = "post_image")
private String postImage;
@Column(name = "likes")
private Long likes;
@Column(name = "views")
private Long views;
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getPostImage() {
return postImage;
}
public String getPostImageComplete() {
return " https://hashtekbucket.s3.us-east-2.amazonaws.com/" + postImage;
}
public void setPostImage(String postImage) {
this.postImage = postImage;
}
public Long getLikes() {
return likes;
}
public void setLikes(Long likes) {
this.likes = likes;
}
public Long getViews() {
return views;
}
public void setViews(Long views) {
this.views = views;
}
}
UserPostController.java:
package com.Application.controller;
import com.Application.models.User;
import com.Application.models.UserPosts;
import com.Application.payload.response.MessageResponse;
import com.Application.repository.UserPostsRepository;
import com.Application.security.jwt.JwtUtils;
import com.Application.security.services.CustomUserDetails;
import com.Application.security.services.CustomUserDetailsService;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import static org.apache.http.entity.ContentType.*;
import static org.apache.http.entity.ContentType.IMAGE_GIF;
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api/posts")
public class UserPostsController {
private static final String AUTH_HEADER = "authorization";
// @Autowired
// private final UserPostsRepository userPostRepo;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
JwtUtils jwtUtils;
/** AWS CREDENTIALS SOURCE
For AWS Builder **/
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String accessSecret;
@Value("${cloud.aws.region.static}")
private String region;
// public UserPostsController(UserPostsRepository userPostRepo) {
// this.userPostRepo = userPostRepo;
// }
@PostMapping(
path = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity < ? > userPostUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes,
Model model,
UserPosts userPosts, HttpServletRequest request, User user) {
//Check if POST is empty
if (file.isEmpty()) {
return ResponseEntity.ok(new MessageResponse("Please select a file to upload"));
}
isImage(file);
//Hard coded bucketName -> linked to AWS
String bucketName = "hashtekbucket";
//Add timestamp to name to prevent duplicate names
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
//getting aws access
AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
//Building S3Client connection
AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
.withRegion(Regions.US_EAST_2)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region).build();
try {
// //PUSH TO BUCKET
File fileObj = convertMultiPartFileToFile(file);
s3Client.putObject(new PutObjectRequest(bucketName, fileName, fileObj));
fileObj.delete();
//Show Successful Upload
redirectAttributes.addFlashAttribute("message_2", fileName + ": SuccessFully Uploaded On AWS S3");
//JWT Token retrieval from HTTP request header
final String authHeader = request.getHeader(AUTH_HEADER);
String username = null;
String jwt = null;
if (authHeader != null && authHeader.startsWith("Bearer")) {
jwt = authHeader.substring(7);
username = jwtUtils.getUserNameFromJwtToken(jwt);
}
//Load logged in Username from JWT Token obtained
CustomUserDetails customUser = (CustomUserDetails) userDetailsService.loadUserByUsername(username);
Long userId = customUser.getId();
UserPosts myUserPost = new UserPosts();
myUserPost.setId(userId);
myUserPost.setPostImage(fileName);
// The code under here is how I saved a user profile by ID, // but I am getting an error in my post repo
// since there is no post Id until a user creates one.
// //Find User by id
// userProfile = userProfRepo.findByUserId(userId);
//
// //Set resource name to
// userProfile.setProfile_banner(fileName);
//
// //Save database changes
// userProfRepo.save(userProfile);
} catch (Exception e) {
e.printStackTrace();
}
model.addAttribute("userPosts", userPosts);
return ResponseEntity.ok(new MessageResponse("File Upload Successful"));
}
private void isImage(MultipartFile file) {
if (!Arrays.asList(
IMAGE_JPEG.getMimeType(),
IMAGE_SVG.getMimeType(),
IMAGE_PNG.getMimeType(),
IMAGE_GIF.getMimeType()).contains(file.getContentType())) {
throw new IllegalStateException("File must be an image [" + file.getContentType() + "]");
}
}
/**
File Conversion
**/
private File convertMultiPartFileToFile(MultipartFile file) {
File convertedFile = new File(file.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
fos.write(file.getBytes());
} catch (IOException e) {
// log.error("Error converting multipartFile to file", e);
}
return convertedFile;
}
}
If anyone could help me at all, any information, tips, or anything, it would mean the world. If I am missing anything that seems important please let me know.
Solution 1:[1]
So basically, I need to figure out how to get the input (both file AND title) at once,
You need to send a POST request to the backend with your image and the title. It HTML 5, the standard way to do this is with a form with an input of type file
There is actually also JavaScript API to set the file input to only accept image files; I think this worked using file extension.
send it to my backend and save it in the Post DB. Am I gonna need to pass the title as an argument in my post controller?
Standard file posts have the name of the file included
Since the UserPost DB is a one to many relationship with a User, I am gonna need to pass that logged in user's ID to my controller so that it gets sent to the post DB, just not sure how.
You should not send the user id. In fact, you should typically be wary of sending the user id to the front end at all. Everything in javascript can be inspected by the user... or someone else who sits at his/her desk.
Instead you can rely on the current session (create on the backend) for user identification.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | ControlAltDel |