From 504df2b15d37571b240d3b92554db2c95ea2056a Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Aug 2020 19:19:33 +0200 Subject: [PATCH] Initial commit --- LICENSE.md | 22 ++++ README.md | 115 ++++++++++++++++++ build.gradle | 24 ++++ settings.gradle | 1 + .../webquiz/engine/WebQuizEngine.java | 13 ++ .../engine/controller/QuizController.java | 55 +++++++++ .../engine/controller/UserController.java | 26 ++++ .../webquiz/engine/dto/AnswerDto.java | 16 +++ .../webquiz/engine/dto/FeedbackDto.java | 28 +++++ .../hyperskill/webquiz/engine/model/Quiz.java | 83 +++++++++++++ .../webquiz/engine/model/QuizCompleted.java | 62 ++++++++++ .../hyperskill/webquiz/engine/model/User.java | 38 ++++++ .../repository/QuizCompletedRepository.java | 10 ++ .../engine/repository/QuizRepository.java | 8 ++ .../engine/repository/UserRepository.java | 8 ++ .../security/SecurityConfiguration.java | 34 ++++++ .../webquiz/engine/service/QuizService.java | 86 +++++++++++++ .../webquiz/engine/service/UserService.java | 47 +++++++ src/main/resources/application.properties | 14 +++ 19 files changed, 690 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/main/java/org/hyperskill/webquiz/engine/WebQuizEngine.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/controller/QuizController.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/controller/UserController.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/dto/AnswerDto.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/dto/FeedbackDto.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/model/QuizCompleted.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/model/User.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/repository/QuizCompletedRepository.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/repository/QuizRepository.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/repository/UserRepository.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/security/SecurityConfiguration.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/service/QuizService.java create mode 100644 src/main/java/org/hyperskill/webquiz/engine/service/UserService.java create mode 100644 src/main/resources/application.properties diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6e6d9eb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2020 David Álvarez González + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d58368b --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# WebQuizEngine +Spring Boot web service to create and solve quizzes using a REST API. +## Register user +To register a new user send a JSON with `email` and `password` via `POST` request to `/api/register`: +``` json +{ +"email": "test@gmail.com", +"password": "secret" +} +``` +If the email is already taken by another user, the service will return the 400 (Bad request) status code. +The email also must have a valid format (with @ and .) and the password must have at least five characters. +## Create quiz +In order to create a quiz send a JSON via `POST` request to `api/quizzes` with this format: +``` json +{ +"title": "The Ultimate Question", +"text": "What is the answer to the Ultimate Question of Life, the Universe and Everything?", +"options": ["Everything goes right","42","2+2=4","11011100"], +"answer": [1] +} +``` +The answer equals [1] corresponds to the second item from the options array (multiple choice questions are supported) + +If the number of options in the quiz is less than 2 the server responds with `400 (Bad request)` status code. + +If everything is correct, the server response is a JSON with four fields: id, title, text and options: +```json +{ + "id": 1, + "title": "The Ultimate Question", + "text": "What is the answer to the Ultimate Question of Life, the Universe and Everything?", + "options": ["Everything goes right","42","2+2=4","11011100"] +} +``` +## Get quizzes +Send a `GET` request to `api/quizzes/{id}` to get quiz by its id. + +Send a `GET` request to `api/quizzes` to get all quizzes. + +Response contains a JSON with quizzes (inside content) and some additional metadata, i.e: + +```json +{ + "totalPages":1, + "totalElements":3, + "last":true, + "first":true, + "sort":{ }, + "number":0, + "numberOfElements":3, + "size":10, + "empty":false, + "pageable": { }, + "content":[ + {"id":102,"title":"Test 1","text":"Text 1","options":["a","b","c"]}, + {"id":103,"title":"Test 2","text":"Text 2","options":["a", "b", "c", "d"]}, + {"id":202,"title":"The Java Logo","text":"What is depicted on the Java logo?", + "options":["Robot","Tea leaf","Cup of coffee","Bug"]} + ] +} +``` + +The API support the navigation through pages by passing the page parameter: `/api/quizzes?page=1` +## Solve quiz +For solving quiz send a JSON via `POST` request to `api/quizzes/{id}/solve`: + +``` json +{ +"answer": [2] +} +``` +Where answer is the indexes of the correct answers, and id is the question id. + +It is also possible to send an empty array [] since some quizzes may not have correct options. + +The service returns a JSON with two fields: `success` (`true` or `false`) and `feedback` (just a string). + +There are three possible responses: + +- If the passed answer is correct: +`{"success":true,"feedback":"Congratulations, you're right!"}` +- If the answer is incorrect: +`{"success":false,"feedback":"Wrong answer! Please, try again."}` +- If the specified quiz does not exist, the server returns the `404 (Not found)` status code. + +## Get completed quizzes +Send a `GET` request to `/api/quizzes/completed` together with the user auth data to get completed quizzes by user. + +It is allowed to solve a quiz multiple times and completions are sorted from the most recent to the oldest. + +Response example: + +```json +{ + "totalPages":1, + "totalElements":5, + "last":true, + "first":true, + "empty":false, + "content":[ + {"id":103,"completedAt":"2019-10-29T21:13:53.779542"}, + {"id":102,"completedAt":"2019-10-29T21:13:52.324993"}, + {"id":101,"completedAt":"2019-10-29T18:59:58.387267"}, + {"id":101,"completedAt":"2019-10-29T18:59:55.303268"}, + {"id":202,"completedAt":"2019-10-29T18:59:54.033801"} + ] +} +``` +## Delete quiz +A user can delete their quiz by sending the `DELETE` request to `/api/quizzes/{id}` + +If the operation was successful, the service returns the `204 (No content)` status code without any content. + +If the specified quiz does not exist, the server returns `404 (Not found)`. If the specified user is not the author of this quiz, the response is the `403 (Forbidden)` status code. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3e67c2f --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'org.springframework.boot' version '2.2.2.RELEASE' + id 'java' +} + +apply plugin: 'io.spring.dependency-management' + +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +sourceSets.main.resources.srcDirs = ["src/main/resources"] + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + runtimeOnly 'com.h2database:h2' + compile("org.springframework.boot:spring-boot-starter-web") +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9032234 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'webquizengine' diff --git a/src/main/java/org/hyperskill/webquiz/engine/WebQuizEngine.java b/src/main/java/org/hyperskill/webquiz/engine/WebQuizEngine.java new file mode 100644 index 0000000..fddf666 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/WebQuizEngine.java @@ -0,0 +1,13 @@ +package org.hyperskill.webquiz.engine; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebQuizEngine { + + public static void main(String[] args) { + SpringApplication.run(WebQuizEngine.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/controller/QuizController.java b/src/main/java/org/hyperskill/webquiz/engine/controller/QuizController.java new file mode 100644 index 0000000..fb18901 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/controller/QuizController.java @@ -0,0 +1,55 @@ +package org.hyperskill.webquiz.engine.controller; + +import org.hyperskill.webquiz.engine.model.Quiz; +import org.hyperskill.webquiz.engine.dto.AnswerDto; +import org.hyperskill.webquiz.engine.dto.FeedbackDto; +import org.hyperskill.webquiz.engine.model.QuizCompleted; +import org.hyperskill.webquiz.engine.service.QuizService; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/quizzes") +public class QuizController { + + private final QuizService quizService; + + public QuizController(QuizService quizService) { + this.quizService = quizService; + } + + @PostMapping + public ResponseEntity createQuiz(@Valid @RequestBody Quiz quiz) { + return ResponseEntity.ok(quizService.createQuiz(quiz)); + } + + @GetMapping + public ResponseEntity> getQuizzes(@RequestParam(defaultValue = "0", required = false) Integer page) { + return ResponseEntity.ok(quizService.getAllQuizzes(page)); + } + + @GetMapping("/{id}") + public ResponseEntity getQuizById(@PathVariable Long id) { + return ResponseEntity.ok(quizService.getQuiz(id)); + } + + @PostMapping("/{id}/solve") + public ResponseEntity solveQuiz(@PathVariable(value="id") Long id, @RequestBody AnswerDto answer){ + return ResponseEntity.ok(quizService.solveQuiz(id, answer)); + } + + @GetMapping(path = "/completed") + public ResponseEntity> getCompletedQuestionForUser(@RequestParam(defaultValue = "0", required = false) Integer page) { + return ResponseEntity.ok(quizService.getCompletedQuestionForUser(page)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteQuiz(@PathVariable Long id) { + quizService.deleteQuiz(id); + return new ResponseEntity<>("Quiz deleted", HttpStatus.NO_CONTENT); + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/controller/UserController.java b/src/main/java/org/hyperskill/webquiz/engine/controller/UserController.java new file mode 100644 index 0000000..4f5ff6b --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/controller/UserController.java @@ -0,0 +1,26 @@ +package org.hyperskill.webquiz.engine.controller; + +import org.hyperskill.webquiz.engine.model.User; +import org.hyperskill.webquiz.engine.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/register") +public class UserController { + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @PostMapping + public ResponseEntity registerUser(@RequestBody @Valid User user) { + userService.registerUser(user); + return new ResponseEntity<>("Registration successful", HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/dto/AnswerDto.java b/src/main/java/org/hyperskill/webquiz/engine/dto/AnswerDto.java new file mode 100644 index 0000000..7d0c9bf --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/dto/AnswerDto.java @@ -0,0 +1,16 @@ +package org.hyperskill.webquiz.engine.dto; + +import java.util.List; + +public class AnswerDto { + + private List answer; + + public List getAnswer() { + return answer; + } + + public void setAnswer(List answer) { + this.answer = answer; + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/dto/FeedbackDto.java b/src/main/java/org/hyperskill/webquiz/engine/dto/FeedbackDto.java new file mode 100644 index 0000000..8c74fcf --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/dto/FeedbackDto.java @@ -0,0 +1,28 @@ +package org.hyperskill.webquiz.engine.dto; + +public class FeedbackDto { + + private boolean success; + private String feedback; + + public FeedbackDto(boolean success, String feedback) { + this.success = success; + this.feedback = feedback; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getFeedback() { + return feedback; + } + + public void setFeedback(String feedback) { + this.feedback = feedback; + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java b/src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java new file mode 100644 index 0000000..58a1bd6 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java @@ -0,0 +1,83 @@ +package org.hyperskill.webquiz.engine.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.*; + +@Entity +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Quiz { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Long id; + + @NotNull + @NotBlank(message = "Title can't be empty") + private String title; + + @NotNull + @NotBlank(message = "Text can't be empty") + private String text; + + @NotNull + @Size(min = 2, message = "At least two options needed") + @ElementCollection + private List options; + + @ElementCollection + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private List answer; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @ManyToOne + private User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public List getAnswer() { + return answer; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/model/QuizCompleted.java b/src/main/java/org/hyperskill/webquiz/engine/model/QuizCompleted.java new file mode 100644 index 0000000..226d7ec --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/model/QuizCompleted.java @@ -0,0 +1,62 @@ +package org.hyperskill.webquiz.engine.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Entity +public class QuizCompleted { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @JsonIgnore + private Long id; + + @NotNull + @JsonProperty("id") + private Long questionId; + + @NotNull + private LocalDateTime completedAt; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_email", nullable = false) + private User user; + + public QuizCompleted() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getQuestionId() { + return questionId; + } + + public void setQuestionId(Long questionId) { + this.questionId = questionId; + } + + public LocalDateTime getCompletedAt() { + return completedAt; + } + + public void setCompletedAt(LocalDateTime completedAt) { + this.completedAt = completedAt; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } +} diff --git a/src/main/java/org/hyperskill/webquiz/engine/model/User.java b/src/main/java/org/hyperskill/webquiz/engine/model/User.java new file mode 100644 index 0000000..e2612f4 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/model/User.java @@ -0,0 +1,38 @@ +package org.hyperskill.webquiz.engine.model; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Entity +public class User { + @Id + @NotNull + @Email(regexp = ".+@.+\\..+", message = "Given string is not a valid email") + private String email; + + @NotNull + @Size(min = 5, message = "Password must be at least 5 characters long") + private String password; + + public User() { + } + + 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; + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/repository/QuizCompletedRepository.java b/src/main/java/org/hyperskill/webquiz/engine/repository/QuizCompletedRepository.java new file mode 100644 index 0000000..11fc99e --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/repository/QuizCompletedRepository.java @@ -0,0 +1,10 @@ +package org.hyperskill.webquiz.engine.repository; + +import org.hyperskill.webquiz.engine.model.QuizCompleted; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuizCompletedRepository extends JpaRepository { + Page findByUserEmail(String email, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/repository/QuizRepository.java b/src/main/java/org/hyperskill/webquiz/engine/repository/QuizRepository.java new file mode 100644 index 0000000..a893851 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/repository/QuizRepository.java @@ -0,0 +1,8 @@ +package org.hyperskill.webquiz.engine.repository; + +import org.hyperskill.webquiz.engine.model.Quiz; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuizRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/repository/UserRepository.java b/src/main/java/org/hyperskill/webquiz/engine/repository/UserRepository.java new file mode 100644 index 0000000..076db76 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/repository/UserRepository.java @@ -0,0 +1,8 @@ +package org.hyperskill.webquiz.engine.repository; + +import org.hyperskill.webquiz.engine.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findByEmail(String email); +} diff --git a/src/main/java/org/hyperskill/webquiz/engine/security/SecurityConfiguration.java b/src/main/java/org/hyperskill/webquiz/engine/security/SecurityConfiguration.java new file mode 100644 index 0000000..cc38def --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/security/SecurityConfiguration.java @@ -0,0 +1,34 @@ +package org.hyperskill.webquiz.engine.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/h2-console/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/register").permitAll() + .antMatchers(HttpMethod.POST, "/actuator/shutdown").permitAll() + .anyRequest().authenticated().and() + .csrf().disable() + .headers().frameOptions().disable().and() + .httpBasic().and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/service/QuizService.java b/src/main/java/org/hyperskill/webquiz/engine/service/QuizService.java new file mode 100644 index 0000000..900b458 --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/service/QuizService.java @@ -0,0 +1,86 @@ +package org.hyperskill.webquiz.engine.service; + +import org.hyperskill.webquiz.engine.model.Quiz; +import org.hyperskill.webquiz.engine.dto.AnswerDto; +import org.hyperskill.webquiz.engine.dto.FeedbackDto; +import org.hyperskill.webquiz.engine.model.QuizCompleted; +import org.hyperskill.webquiz.engine.repository.QuizCompletedRepository; +import org.hyperskill.webquiz.engine.repository.QuizRepository; +import org.hyperskill.webquiz.engine.repository.UserRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.time.LocalDateTime; +import java.util.HashSet; + +@Service +public class QuizService { + + private final QuizRepository quizRepository; + private final UserRepository userRepository; + private final QuizCompletedRepository quizCompletedRepository; + + public QuizService(QuizRepository quizRepository, UserRepository userRepository, QuizCompletedRepository quizCompletedRepository) { + this.quizRepository = quizRepository; + this.userRepository = userRepository; + this.quizCompletedRepository = quizCompletedRepository; + } + + @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Quiz not found!") + static class QuizNotFoundException extends RuntimeException { + } + + @ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "You are not the creator of the quiz!") + static class QuizWrongUserException extends RuntimeException { + } + + public Quiz createQuiz(Quiz quiz) { + quiz.setUser(userRepository.findByEmail(getUserMail())); + return quizRepository.save(quiz); + } + + public Quiz getQuiz(Long id) { + return quizRepository.findById(id).orElseThrow(QuizNotFoundException::new); + } + + public Page getAllQuizzes(Integer page) { + return quizRepository.findAll(PageRequest.of(page, 10)); + } + + public FeedbackDto solveQuiz(Long id, AnswerDto answer) { + Quiz quiz = quizRepository.findById(id).orElseThrow(QuizNotFoundException::new); + if (new HashSet<>(quiz.getAnswer()).equals(new HashSet<>(answer.getAnswer()))) { + QuizCompleted quizCompleted = new QuizCompleted(); + quizCompleted.setQuestionId(quiz.getId()); + quizCompleted.setCompletedAt(LocalDateTime.now()); + quizCompleted.setUser(userRepository.findByEmail(getUserMail())); + quizCompletedRepository.save(quizCompleted); + return new FeedbackDto(true, "Congratulations, you're right!"); + } else { + return new FeedbackDto(false, "Wrong answer! Please, try again."); + } + } + + public Page getCompletedQuestionForUser(Integer page) { + return quizCompletedRepository.findByUserEmail(getUserMail(), PageRequest.of(page, 10, Sort.by(Sort.Direction.DESC, "completedAt"))); + } + + public void deleteQuiz(Long id) { + Quiz quiz = quizRepository.findById(id).orElseThrow(QuizNotFoundException::new); + + if (quiz.getUser().getEmail().equalsIgnoreCase(getUserMail())) { + quizRepository.deleteById(id); + } else { + throw new QuizWrongUserException(); + } + } + + private String getUserMail() { + return SecurityContextHolder.getContext().getAuthentication().getName(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hyperskill/webquiz/engine/service/UserService.java b/src/main/java/org/hyperskill/webquiz/engine/service/UserService.java new file mode 100644 index 0000000..8e7db6b --- /dev/null +++ b/src/main/java/org/hyperskill/webquiz/engine/service/UserService.java @@ -0,0 +1,47 @@ +package org.hyperskill.webquiz.engine.service; + +import org.hyperskill.webquiz.engine.model.User; +import org.hyperskill.webquiz.engine.repository.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseStatus; + +import java.util.List; + +@Component +public class UserService implements UserDetailsService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Email already taken!") + static class EmailTakenException extends RuntimeException { + } + + public void registerUser(User user) { + if (userRepository.existsById(user.getEmail())) { + throw new EmailTakenException(); + } + user.setPassword(passwordEncoder.encode(user.getPassword())); + userRepository.save(user); + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email); + if (!userRepository.existsById(user.getEmail())) { + throw new UsernameNotFoundException(email + " not found"); + } + return new org.springframework.security.core.userdetails. + User(user.getEmail(), user.getPassword(), List.of()); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..7f50f0e --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,14 @@ +server.port=8889 +management.endpoints.web.exposure.include=* +management.endpoint.shutdown.enabled=true +spring.datasource.url=jdbc:h2:file:../quizdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +spring.h2.console.enabled=true +spring.h2.console.settings.trace=false +spring.h2.console.settings.web-allow-others=false \ No newline at end of file