Unverified Commit fb335938 authored by Khalid Ali's avatar Khalid Ali Committed by GitHub
Browse files

Feature/16-Frontend-Post (#19)

Add the ability to handle a POST request from the frontend.

---
* Send CSRF, Fix 500 Error, Update gitignore

* Get title from Google Books API

* Include time when creating Listing

* Bulk Update - Multiple images can now be uploaded and saved in the DB.

* Update Travis JDK to 11

* Update tests to work with new code

- Commented out two tests, noted as a minor issue
- Downgraded hashcode function, noted as minor issue

* Fix SonarQube Code Smells - Optimized imports and removed comment blocks

* Small fix to get tests working again

* Moved RestTemplate to class scope to fix last two tests

* Add newListing test and move model/entity conversion out of controller

* Travis back to JDK 8

* Travis to OpenJDK 11

* Fix NullPointerException

* Trying Travis OpenJDK8

* Fix broken Rest Template.
parent 0d020ce9
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# Custom
deploy/*
kompose
src/main/resources/static/**
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
......
......@@ -3,7 +3,7 @@ sudo: false
install: true
jdk:
- oraclejdk8
- openjdk8
addons:
sonarcloud:
......
......@@ -48,6 +48,7 @@ dependencies {
implementation('com.fasterxml.jackson.datatype:jackson-datatype-joda')
implementation('org.springframework.security:spring-security-config')
implementation('org.modelmapper:modelmapper:2.3.1')
compile('com.allanditzel:spring-security-csrf-token-filter:1.1')
compile('org.springframework.security:spring-security-cas')
compile('org.springframework.security:spring-security-test')
compile('io.springfox:springfox-swagger2:2.9.2')
......
version: '3.5'
version: '3'
services:
bs-frontend:
image: bookshare-frontend
expose:
- "8081"
ports:
- "8081:8081"
networks:
- backend
bs-backend:
image: bookshare-backend
depends_on:
- db
expose:
- "9090"
ports:
- "9090:9090"
networks:
- backend
flyway:
image: boxfuse/flyway:5.2.1
command: -url=jdbc:postgresql://db:26257/testdb?sslmode=disable -user=user17 -password= -connectRetries=60 migrate
......@@ -22,6 +40,7 @@ services:
- "8080:8080"
networks:
- roachnet
- backend
db-init:
image: cockroachdb/cockroach:v2.1.4
networks:
......@@ -34,3 +53,4 @@ services:
- db
networks:
roachnet:
backend:
......@@ -17,9 +17,9 @@ import org.springframework.security.cas.authentication.CasAuthenticationProvider
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.filter.ForwardedHeaderFilter;
import javax.servlet.http.HttpSessionEvent;
......@@ -37,8 +37,13 @@ public class BookshareApplication {
}
@Bean
FilterRegistrationBean forwardedHeaderFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
FilterRegistrationBean<ForwardedHeaderFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new ForwardedHeaderFilter());
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterRegistrationBean;
......@@ -54,12 +59,13 @@ public class BookshareApplication {
@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(
public CasAuthenticationEntryPoint authenticationEntryPoint(
ServiceProperties sP) {
// URL where user will be redirected to for authentication
CasAuthenticationEntryPoint entryPoint
= new CasAuthenticationEntryPoint();
// entryPoint.setLoginUrl("https://boiling-waters-26199.herokuapp.com/https://login.gmu.edu/login");
entryPoint.setLoginUrl("https://login.gmu.edu/login");
entryPoint.setServiceProperties(sP);
return entryPoint;
......@@ -106,7 +112,7 @@ public class BookshareApplication {
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix("https://login.gmu.edu");
singleSignOutFilter.setCasServerUrlPrefix("https://login.gmu.edu/");
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
......
package com.gmu.bookshare.config;
import com.allanditzel.springframework.security.web.csrf.CsrfTokenResponseHeaderBindingFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
......@@ -9,6 +10,7 @@ import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
......@@ -16,6 +18,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
......@@ -33,7 +36,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
private LogoutFilter logoutFilter;
@Autowired
public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, AuthenticationEntryPoint eP,
public SecurityConfig(CasAuthenticationProvider casAuthenticationProvider, CasAuthenticationEntryPoint eP,
LogoutFilter lF, SingleSignOutFilter ssF) {
this.authenticationProvider = casAuthenticationProvider;
this.authenticationEntryPoint = eP;
......@@ -44,12 +47,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
// .cors()
// .and()
// .csrf()
// .disable()
.authorizeRequests()
.regexMatchers("/secured.*", "/login", "/bs/api/.*")
// .regexMatchers("/secured.*", "/login", "/bs/api/.*")
.regexMatchers("/secured.*", "/bs/api/.*")
.authenticated()
.and()
.authorizeRequests()
......@@ -62,7 +66,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.logout().logoutSuccessUrl("/bs/api/logout")
.and()
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
.addFilterBefore(logoutFilter, LogoutFilter.class);
.addFilterBefore(logoutFilter, LogoutFilter.class)
.addFilterAfter(new CsrfTokenResponseHeaderBindingFilter(), CsrfFilter.class);
}
@Override
......@@ -82,13 +87,14 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
filter.setAuthenticationManager(authenticationManager());
return filter;
}
//
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS", "DELETE", "PUT", "PATCH"));
configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Origin", "Content-Type", "Accept",
"Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
......
......@@ -15,7 +15,7 @@ import java.util.Date;
public class BidEntity {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
......
package com.gmu.bookshare.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@RequiredArgsConstructor
@Entity
@Table(name = "Image")
public class ImageEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "listingId")
private ListingEntity listing;
@NonNull
@Column(name = "image")
private byte[] image;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ImageEntity)) return false;
return id != null && id.equals(((ImageEntity) o).id);
}
@Override
public int hashCode() {
return image.length * image[image.length / 2];
}
}
package com.gmu.bookshare.entity;
import lombok.*;
import com.gmu.bookshare.model.ListingDto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import javax.persistence.*;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
@Data
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "Listing")
public class ListingEntity {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "course")
private String course;
@NonNull
@Column(name = "isbn")
private int isbn;
private long isbn;
@NonNull
@Column(name = "condition")
private int condition;
@Column(name = "accessCode")
private boolean accessCode;
private int accessCode;
@NonNull
@Column(name = "price")
private double price;
@Column(name = "image")
private byte[] image;
@Column(name = "description")
private String description;
......@@ -67,10 +62,18 @@ public class ListingEntity {
)
private Set<BidEntity> bids = new HashSet<>();
@OneToMany(
mappedBy = "image",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<ImageEntity> images = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shareUserId")
private ShareUser shareUser;
public void addBid(BidEntity bid) {
bids.add(bid);
bid.setListing(this);
......@@ -81,6 +84,11 @@ public class ListingEntity {
bid.setListing(null);
}
public void addImages(ImageEntity image) {
images.add(image);
image.setListing(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
......@@ -90,6 +98,19 @@ public class ListingEntity {
@Override
public int hashCode() {
return 41;
return 71;
}
public ListingDto toDto() {
ListingDto listingDto = new ListingDto();
listingDto.setCourse(course);
listingDto.setIsbn(isbn);
listingDto.setCondition(condition);
listingDto.setAccessCode(accessCode);
listingDto.setPrice(price);
listingDto.setDescription(description);
listingDto.setTitle(title);
return listingDto;
}
}
......@@ -14,7 +14,7 @@ import java.util.Set;
public class ShareUser {
@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
......
......@@ -2,12 +2,14 @@ package com.gmu.bookshare.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.gmu.bookshare.entity.ListingEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@Builder
......@@ -23,19 +25,19 @@ public class ListingDto {
private String course;
@JsonProperty("isbn")
private int isbn;
private long isbn;
@JsonProperty("condition")
private int condition;
@JsonProperty("accessCode")
private boolean accessCode;
private int accessCode;
@JsonProperty("price")
private double price;
@JsonProperty("image")
private byte[] image;
private List<byte[]> image;
@JsonProperty("description")
private String description;
......@@ -45,4 +47,23 @@ public class ListingDto {
@JsonProperty("title")
private String title;
public ListingEntity toEntity() {
ListingEntity listingEntity = new ListingEntity();
listingEntity.setCourse(course);
listingEntity.setIsbn(isbn);
listingEntity.setCondition(condition);
listingEntity.setAccessCode(accessCode);
listingEntity.setPrice(price);
listingEntity.setDescription(description);
listingEntity.setCreateDate(new Date());
listingEntity.setTitle(title);
return listingEntity;
}
public boolean checkFields() {
return isbn >= 100000000 && !(price <= 0) && title != null;
}
}
......@@ -9,5 +9,5 @@ public interface ListingRepository extends JpaRepository<ListingEntity, Long> {
List<ListingEntity> findByTitle(String s);
List<ListingEntity> findByIsbn(int isbn);
List<ListingEntity> findByIsbn(long isbn);
}
package com.gmu.bookshare.service;
import com.gmu.bookshare.entity.BidEntity;
import com.gmu.bookshare.entity.ImageEntity;
import com.gmu.bookshare.entity.ListingEntity;
import com.gmu.bookshare.entity.ShareUser;
import com.gmu.bookshare.error.ListingNotFoundException;
import com.gmu.bookshare.model.ListingDto;
import com.gmu.bookshare.persistence.ListingRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
......@@ -23,8 +26,20 @@ public class ListingService {
return listingRepository.findAll();
}
public ListingEntity addListing(ListingEntity newListingEntity) {
return listingRepository.save(newListingEntity);
public ListingEntity addListing(ListingDto listingDto, List<byte[]> images, ShareUser user) {
ListingEntity listingEntity = listingDto.toEntity();
user.addListing(listingEntity);
if (images != null) {
images.forEach(x -> {
ImageEntity image = new ImageEntity(x);
listingEntity.addImages(image);
});
}
return listingRepository.save(listingEntity);
}
......@@ -33,7 +48,7 @@ public class ListingService {
.orElseThrow(() -> new ListingNotFoundException(id));
}
ListingEntity getIsbn(int isbn) {
ListingEntity getIsbn(long isbn) {
return listingRepository.findByIsbn(isbn).get(0);
}
......
package com.gmu.bookshare.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gmu.bookshare.entity.BidEntity;
import com.gmu.bookshare.entity.ListingEntity;
import com.gmu.bookshare.entity.ShareUser;
......@@ -9,22 +11,31 @@ import com.gmu.bookshare.model.ShareUserDto;
import com.gmu.bookshare.service.BidService;
import com.gmu.bookshare.service.ListingService;
import com.gmu.bookshare.service.ShareUserService;
import com.gmu.bookshare.wrapper.DataWrapper;
import com.gmu.bookshare.wrapper.FormWrapper;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
......@@ -36,9 +47,15 @@ public class BookshareApiController {
private final BidService bidService;
private final ShareUserService shareUserService;
@Value("${secret.apikey}")
private String API_KEY;
@Autowired
private ModelMapper modelMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
public BookshareApiController(ListingService listingService, BidService bidService,
ShareUserService shareUserService) {
......@@ -55,25 +72,61 @@ public class BookshareApiController {
.collect(Collectors.toList());
}
@PostMapping(value = "/listing", consumes = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/listing")
@ResponseStatus(HttpStatus.CREATED)
public ListingDto newListing(@RequestBody ListingDto listingDto) {
public ResponseEntity<?> newListing(@ModelAttribute FormWrapper model) throws Exception {
// Convert data back to JSON
ObjectMapper dataMapper = new ObjectMapper();
DataWrapper dataWrapper = dataMapper.readValue(model.getData(), DataWrapper.class);
// Convert DataWrapper to ListingDto
ListingDto listingDto = dataWrapper.toDto();
listingDto.setTitle(getTitleFromIsbn(listingDto.getIsbn()));
// Make sure necessary fields are provided
if (!listingDto.checkFields()) {
if (listingDto.getTitle() == null)
return new ResponseEntity<>("Title was not found from ISBN", HttpStatus.BAD_REQUEST);
else if (listingDto.getPrice() <= 0)
return new ResponseEntity<>("Price was zero or less", HttpStatus.BAD_REQUEST);
else if (listingDto.getIsbn() < 100000000)
return new ResponseEntity<>("Not a valid ISBN", HttpStatus.BAD_REQUEST);
return new ResponseEntity<>("Did not provide sufficient fields", HttpStatus.BAD_REQUEST);
}
// Get array of images
List<byte[]> images = new ArrayList<>();
if (model.getImages() != null) {
for (MultipartFile image : model.getImages()) {
// Set image if provided
if (image != null && !image.isEmpty()) {
try {
images.add(image.getBytes());
Path path = Paths.get(image.getName());
Files.write(path, image.getBytes());
} catch (IOException e) {
System.err.println("ERROR: Could not get bytes off of image");
}
}
}
}
ShareUser user = shareUserService.getShareUser();
ListingEntity listingEntity = convertToEntity(listingDto);
user.addListing(listingEntity);
ListingEntity listingCreated = listingService.addListing(listingEntity);
return convertToDto(listingCreated);
listingService.addListing(listingDto, images, user);
return new ResponseEntity<>("Successfully uploaded!", HttpStatus.CREATED);
}
@GetMapping(value = "/listing/{id}")
ListingDto getOne(@PathVariable Long id) {
public ListingDto getOne(@PathVariable Long id) {
return convertToDto(listingService.getById(id));
}