Initial commit
This commit is contained in:
22
LICENSE.md
Normal file
22
LICENSE.md
Normal file
@@ -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.
|
||||||
115
README.md
Normal file
115
README.md
Normal file
@@ -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.
|
||||||
24
build.gradle
Normal file
24
build.gradle
Normal file
@@ -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")
|
||||||
|
}
|
||||||
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'webquizengine'
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<Quiz> createQuiz(@Valid @RequestBody Quiz quiz) {
|
||||||
|
return ResponseEntity.ok(quizService.createQuiz(quiz));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Page<Quiz>> getQuizzes(@RequestParam(defaultValue = "0", required = false) Integer page) {
|
||||||
|
return ResponseEntity.ok(quizService.getAllQuizzes(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<Quiz> getQuizById(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(quizService.getQuiz(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/solve")
|
||||||
|
public ResponseEntity<FeedbackDto> solveQuiz(@PathVariable(value="id") Long id, @RequestBody AnswerDto answer){
|
||||||
|
return ResponseEntity.ok(quizService.solveQuiz(id, answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/completed")
|
||||||
|
public ResponseEntity<Page<QuizCompleted>> getCompletedQuestionForUser(@RequestParam(defaultValue = "0", required = false) Integer page) {
|
||||||
|
return ResponseEntity.ok(quizService.getCompletedQuestionForUser(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<String> deleteQuiz(@PathVariable Long id) {
|
||||||
|
quizService.deleteQuiz(id);
|
||||||
|
return new ResponseEntity<>("Quiz deleted", HttpStatus.NO_CONTENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> registerUser(@RequestBody @Valid User user) {
|
||||||
|
userService.registerUser(user);
|
||||||
|
return new ResponseEntity<>("Registration successful", HttpStatus.OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.hyperskill.webquiz.engine.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AnswerDto {
|
||||||
|
|
||||||
|
private List<Integer> answer;
|
||||||
|
|
||||||
|
public List<Integer> getAnswer() {
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAnswer(List<Integer> answer) {
|
||||||
|
this.answer = answer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java
Normal file
83
src/main/java/org/hyperskill/webquiz/engine/model/Quiz.java
Normal file
@@ -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<String> options;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||||
|
private List<Integer> 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<String> getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptions(List<String> options) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getAnswer() {
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/org/hyperskill/webquiz/engine/model/User.java
Normal file
38
src/main/java/org/hyperskill/webquiz/engine/model/User.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<QuizCompleted, Long> {
|
||||||
|
Page<QuizCompleted> findByUserEmail(String email, Pageable pageable);
|
||||||
|
}
|
||||||
@@ -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<Quiz, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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, String> {
|
||||||
|
User findByEmail(String email);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Quiz> 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<QuizCompleted> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main/resources/application.properties
Normal file
14
src/main/resources/application.properties
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user